diff --git a/.config/Code/User/keybindings.json b/.config/Code/User/keybindings.json index 1a12b65..47b18c1 100644 --- a/.config/Code/User/keybindings.json +++ b/.config/Code/User/keybindings.json @@ -390,7 +390,7 @@ "command": "-workbench.action.gotoLine" }, { - "key": "ctrl+g", + "key": "alt+space", "command": "relative-goto.goto" }, { diff --git a/.config/Code/User/settings.json b/.config/Code/User/settings.json index 4e5235c..7f3d995 100644 --- a/.config/Code/User/settings.json +++ b/.config/Code/User/settings.json @@ -1,126 +1,132 @@ -{ - // - // Appearance - "workbench.colorTheme": "Monokai Pro", - "workbench.iconTheme": "Monokai Pro Icons", - "editor.fontFamily": "Fira Code, Menlo, Monaco, 'Courier New', monospace", - "editor.fontSize": 18, - "editor.fontWeight": "normal", - "editor.fontLigatures": true, - "editor.stickyScroll.enabled": true, - "editor.minimap.renderCharacters": false, - "editor.suggestSelection": "first", - "vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue", - "window.titleBarStyle": "custom", - "workbench.sideBar.location": "right", - // - // Excluded files and directories - "files.exclude": { - "**/.classpath": true, - "**/.project": true, - "**/.settings": true, - "**/.factorypath": true - }, - "files.watcherExclude": { - "**/.bloop": true, - "**/.metals": true, - "**/.ammonite": true - }, - // - // File associations with languages - "files.associations": { - "*.pgpass" : "yaml", - }, - // - // Settings Sync - "settingsSync.ignoredSettings": [ - "sshfs.configs", - "editor.fontFamily" - ], - // - // Remote SSH - "remote.SSH.remotePlatform": { - "lab-pc07": "linux", - "lab-pc28": "linux", - "lab-pc01": "linux", - "wasteside": "linux", - "sralab": "linux" - }, - "remote.SSH.serverInstallPath": { - "sralab": "/srv/scratch/paul.aumann", - "lab-pc00": "/srv/scratch/paul.aumann", - "lab-pc44": "/srv/scratch/paul.aumann", - }, - // - // Python - "python.languageServer": "Default", - "python.createEnvironment.trigger": "off", - "[python]": { - "editor.formatOnSave": true, - "editor.rulers": [ - 120 - ], - "editor.defaultFormatter": "ms-python.black-formatter", - "editor.codeActionsOnSave": { - "source.organizeImports": "explicit" - } - }, - "black-formatter.args": [ - "--line-length", - "120" - ], - // - // Jupyter Notebooks - "notebook.cellToolbarLocation": { - "default": "right", - "jupyter-notebook": "left" - }, - "workbench.editorAssociations": { - "*.ipynb": "jupyter-notebook" - }, - // - // R - "r.lsp.diagnostics": false, - // - // JSON - "[json][jsonc]": { - "editor.defaultFormatter": "vscode.json-language-features", - }, - "prettier.singleQuote": true, - // - // Webdev - "[typescript][typescriptreact][javascript][html]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, - "editor.indentSize": "tabSize", - "editor.tabSize": 2, - "emmet.showAbbreviationSuggestions": false, - "editor.linkedEditing": true, - "editor.codeActionsOnSave": { - // "source.organizeImports": "always" - } - }, - // - // Hex Editor - "hexeditor.columnWidth": 4, - "hexeditor.showDecodedText": true, - "hexeditor.defaultEndianness": "little", - "hexeditor.inspectorType": "aside", - // - // Typst - "typst-lsp.exportPdf": "onType", - /// - // Various settings - "explorer.confirmDragAndDrop": false, - "workbench.startupEditor": "none", - "security.workspace.trust.untrustedFiles": "open", - "emmet.showExpandedAbbreviation": "never", - "git.openRepositoryInParentFolders": "never", - "editor.selectionClipboard": false, - "git.blame.editorDecoration.enabled": true, - "python.analysis.typeCheckingMode": "basic", - "workbench.editor.empty.hint": "hidden", - "python.defaultInterpreterPath": "/home/paul/.pyenv/versions/3.12.5/envs/wasteside-tools", - "editor.lineNumbers": "relative", - "github.copilot.nextEditSuggestions.enabled": true, +{ + "workbench.iconTheme": "Default Light Modern Icons", + "editor.fontFamily": "JetBrains Mono Semibold, Fira Code, Menlo, Monaco, 'Courier New', monospace", + "editor.fontSize": 18, + "editor.fontWeight": "normal", + "editor.fontLigatures": true, + "editor.stickyScroll.enabled": true, + "editor.minimap.renderCharacters": false, + "editor.suggestSelection": "first", + "vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue", + "window.titleBarStyle": "custom", + "workbench.sideBar.location": "right", + // + // Excluded files and directories + "files.exclude": { + "**/.classpath": true, + "**/.project": true, + "**/.settings": true, + "**/.factorypath": true + }, + "files.watcherExclude": { + "**/.bloop": true, + "**/.metals": true, + "**/.ammonite": true + }, + // + // File associations with languages + "files.associations": { + "*.pgpass" : "yaml", + }, + // + // Settings Sync + "settingsSync.ignoredSettings": [ + "sshfs.configs", + "editor.fontFamily" + ], + // + // Remote SSH + "remote.SSH.remotePlatform": { + "lab-pc07": "linux", + "lab-pc28": "linux", + "lab-pc01": "linux", + "wasteside": "linux", + "sralab": "linux" + }, + "remote.SSH.serverInstallPath": { + "sralab": "/srv/scratch/paul.aumann", + "lab-pc00": "/srv/scratch/paul.aumann", + "lab-pc44": "/srv/scratch/paul.aumann", + }, + // + // Python + "python.languageServer": "Default", + "python.createEnvironment.trigger": "off", + "[python]": { + "editor.formatOnSave": true, + "editor.rulers": [ + 120 + ], + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } + }, + "black-formatter.args": [ + "--line-length", + "120" + ], + // + // Jupyter Notebooks + "notebook.cellToolbarLocation": { + "default": "right", + "jupyter-notebook": "left" + }, + "workbench.editorAssociations": { + "*.ipynb": "jupyter-notebook" + }, + // + // R + "r.lsp.diagnostics": false, + // + // JSON + "[json][jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features", + }, + "prettier.singleQuote": true, + // + // Webdev + "[typescript][typescriptreact][javascript][html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.indentSize": "tabSize", + "editor.tabSize": 2, + "emmet.showAbbreviationSuggestions": false, + "editor.linkedEditing": true, + "editor.codeActionsOnSave": { + // "source.organizeImports": "always" + } + }, + // + // Hex Editor + "hexeditor.columnWidth": 4, + "hexeditor.showDecodedText": true, + "hexeditor.defaultEndianness": "little", + "hexeditor.inspectorType": "aside", + // + // Typst + "typst-lsp.exportPdf": "onType", + /// + // Various settings + "explorer.confirmDragAndDrop": false, + "workbench.startupEditor": "none", + "security.workspace.trust.untrustedFiles": "open", + "emmet.showExpandedAbbreviation": "never", + "git.openRepositoryInParentFolders": "never", + "editor.selectionClipboard": false, + "git.blame.editorDecoration.enabled": true, + "python.analysis.typeCheckingMode": "basic", + "workbench.editor.empty.hint": "hidden", + "python.defaultInterpreterPath": "/home/paul/.pyenv/versions/3.12.5/envs/wasteside-tools", + "editor.lineNumbers": "relative", + "github.copilot.nextEditSuggestions.enabled": true, + "json.schemaDownload.trustedDomains": { + "https://schemastore.azurewebsites.net/": true, + "https://raw.githubusercontent.com/": true, + "https://www.schemastore.org/": true, + "https://json.schemastore.org/": true, + "https://json-schema.org/": true, + "https://vicinae.com/schemas/config.json": true + }, + "workbench.colorTheme": "Monokai Pro", } \ No newline at end of file diff --git a/.config/_default.jsonc b/.config/_default.jsonc new file mode 120000 index 0000000..263178a --- /dev/null +++ b/.config/_default.jsonc @@ -0,0 +1 @@ +vicinae/_default.jsonc \ No newline at end of file diff --git a/.config/appeareance.jsonc b/.config/appeareance.jsonc new file mode 120000 index 0000000..4876596 --- /dev/null +++ b/.config/appeareance.jsonc @@ -0,0 +1 @@ +vicinae/appeareance.jsonc \ No newline at end of file diff --git a/.config/code-flags.conf b/.config/code-flags.conf index 51bdd86..d145ccc 100644 --- a/.config/code-flags.conf +++ b/.config/code-flags.conf @@ -1,2 +1,2 @@ ---enable-features=UseOzonePlatform +# --enable-features=UseOzonePlatform --ozone-platform=wayland diff --git a/.config/darkman/config.yaml b/.config/darkman/config.yaml new file mode 100644 index 0000000..c87e474 --- /dev/null +++ b/.config/darkman/config.yaml @@ -0,0 +1,3 @@ +lat: 52.375893 +lng: 9.732010 +dbusserver: true \ No newline at end of file diff --git a/.config/dunst/dunstrc b/.config/dunst/dunstrc index 0cdd1ab..64d4e48 100644 --- a/.config/dunst/dunstrc +++ b/.config/dunst/dunstrc @@ -4,7 +4,7 @@ ### Display ### # Which monitor should the notifications be displayed on. - monitor = DP-2 + monitor = DP-1 # Display notification on focused monitor. Possible modes are: # mouse: follow mouse pointer @@ -196,7 +196,8 @@ max_icon_size = 50 # Paths to default icons. - icon_path = /usr/share/icons/Adwaita/16x16/mimetypes/:/usr/share/icons/Papirus-Dark/16x16/actions/ + icon_path = "/usr/share/icons/Adwaita/symbolic/actions:/home/paul/.config/dunst/icons" + # enable_recursive_icon_lookup = true default_icon = "/home/paul/.config/dunst/default.svg" diff --git a/.config/dunst/icons/homeassistant.svg b/.config/dunst/icons/homeassistant.svg new file mode 100644 index 0000000..efddb63 --- /dev/null +++ b/.config/dunst/icons/homeassistant.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.config/general.jsonc b/.config/general.jsonc new file mode 120000 index 0000000..1dd33be --- /dev/null +++ b/.config/general.jsonc @@ -0,0 +1 @@ +vicinae/general.jsonc \ No newline at end of file diff --git a/.config/hypr/hyprland.conf b/.config/hypr/hyprland.conf index 9c67f90..038de00 100644 --- a/.config/hypr/hyprland.conf +++ b/.config/hypr/hyprland.conf @@ -16,8 +16,8 @@ ################ # See https://wiki.hyprland.org/Configuring/Monitors/ -monitor=DP-2,3440x1440@144,auto,auto -monitor=DP-3,1920x1080,-1080x-550,1,transform,1 +monitor=DP-1,3440x1440@144,auto,auto +monitor=DP-2,1920x1080,-1080x-550,1,transform,1 monitor=HDMI-A-1,3440x1440@144,auto,auto @@ -31,6 +31,8 @@ monitor=HDMI-A-1,3440x1440@144,auto,auto $terminal = alacritty $fileManager = nautilus --new-window $browser = firefox +$launcher = vicinae toggle +# $launcher = tofi-drun | xargs hyprctl dispatch exec -- ################# @@ -44,14 +46,17 @@ exec-once = hyprpaper exec-once = hypridle exec-once = systemctl --user start hyprpolkitagent exec-once = dunst -exec-once = wl-paste --type text --watch cliphist store # Stores only text data +# exec-once = wl-paste --type text --watch cliphist store # Stores only text data exec-once = waybar +exec-once = darkman run +exec-once = vicinae server # Open programs on special workspaces exec-once = [ workspace special:term silent] $terminal -exec-once = [ workspace special:browser silent] $browser "https://web.whatsapp.com" "https://mail.google.com/mail/u/0/\##inbox/\##wasteside\##container-wasteside" +exec-once = [ workspace special:browser silent] $browser "ext+container:name=Meta&url=https://web.whatsapp.com" "ext+container:name=Wasteside&url=https://mail.google.com" exec-once = [ workspace special:browser silent] gnome-calendar exec-once = [ workspace special:music silent] $terminal -e spotify_player +exec-once = [ workspace special:music silent] $terminal -e wiremix exec-once = [ workspace special:todo silent] clickup exec-once = [ workspace special:files silent] $fileManager exec-once = [ workspace special:files silent] localsend @@ -66,6 +71,9 @@ exec-once = ratbagctl list exec-once = hyprpm enable Hyprspace exec-once = hyprpm reload +# Custom scripts +exec-once = ~/code/ha-notifications/ha-notifications + ############################# ### ENVIRONMENT VARIABLES ### ############################# @@ -74,10 +82,7 @@ exec-once = hyprpm reload env = XCURSOR_SIZE,24 env = HYPRCURSOR_SIZE,24 -env = LIBVA_DRIVER_NAME,nvidia env = XDG_SESSION_TYPE,wayland -env = GBM_BACKEND,nvidia-drm -env = __GLX_VENDOR_LIBRARY_NAME,nvidia env = HYPRSHOT_DIR,/home/paul/screenshots cursor { @@ -119,7 +124,7 @@ decoration { active_opacity = 1.0 inactive_opacity = 1.0 - dim_special = 0.9 + dim_special = 0.7 # Deprecated v0.45 ? # drop_shadow = true @@ -143,15 +148,16 @@ animations { # Default animations, see https://wiki.hyprland.org/Configuring/Animations/ for more - bezier = myBezier, 0.05, 0.9, 0.1, 1.05 + bezier = bz, 0.05, 0.9, 0.1, 1.05 - animation = windows, 1, 7, myBezier - animation = windowsOut, 1, 7, default, popin 80% + animation = windows, 1, 4, bz + animation = windowsOut, 1, 4, default, popin 80% animation = border, 1, 10, default animation = borderangle, 1, 8, default animation = fade, 1, 7, default - animation = workspaces, 1, 6, default - animation = specialWorkspaceIn, 1, 2, default + animation = workspaces, 1, 3, default + + animation = specialWorkspace, 1, 2, default, fade } # See https://wiki.hyprland.org/Configuring/Dwindle-Layout/ for more @@ -169,6 +175,8 @@ master { misc { force_default_wallpaper = 0 # Set to 0 or 1 to disable the anime mascot wallpapers disable_hyprland_logo = true # If true disables the random hyprland logo / anime girl background. :( + + close_special_on_empty = false } @@ -183,14 +191,11 @@ input { kb_model = kb_options = kb_rules = + numlock_by_default = true follow_mouse = 2 sensitivity = 0 # -1.0 - 1.0, 0 means no modification. - - touchpad { - natural_scroll = false - } } # https://wiki.hyprland.org/Configuring/Variables/#gestures @@ -205,6 +210,10 @@ device { sensitivity = -0.5 } +binds { + hide_special_on_workspace_change = true +} + # cursor { # no_warps = true # } @@ -220,14 +229,15 @@ $mainMod = SUPER # Sets "Windows" key as main modifier # Example binds, see https://wiki.hyprland.org/Configuring/Binds/ for more bind = $mainMod, T, exec, $terminal bind = $mainMod, B, exec, $browser -bind = $mainMod, Return, exec, tofi-drun | xargs hyprctl dispatch exec -- +bind = $mainMod, Return, exec, $launcher bind = $mainMod, Q, killactive, bind = $mainMod, F, fullscreen, bind = $mainMod, M, splitratio, exact 0.6, bind = $mainMod, mouse_down, splitratio, 0.1 bind = $mainMod, mouse_up, splitratio, -0.1 bind = $mainMod, E, exec, $fileManager -bind = $mainMod, V, exec, cliphist list | tofi | cliphist decode | wl-copy +# bind = $mainMod, V, exec, cliphist list | tofi | cliphist decode | wl-copy +bind = $mainMod, V, exec, vicinae vicinae://extensions/vicinae/clipboard/history bind = $mainMod, X, togglefloating, bind = $mainMod, P, pin, active bind = $mainMod, J, togglesplit, # dwindle @@ -250,7 +260,7 @@ bind = $mainMod SHIFT, right, movewindow, r bind = $mainMod SHIFT, up, movewindow, u bind = $mainMod SHIFT, down, movewindow, d -# Switch workspaces of primary monitor with mainMod + [1-9] +# Switch workspaces of primary monitor with mainMod + [1-7] bind = $mainMod, 1, workspace, 1 bind = $mainMod, 2, workspace, 2 bind = $mainMod, 3, workspace, 3 @@ -260,7 +270,7 @@ bind = $mainMod, 6, workspace, 6 bind = $mainMod, 7, workspace, 7 bind = $mainMod, 8, workspace, 8 -# Switch workspaces of secondary monitor with mainMod + [F1-9] +# Switch workspaces of secondary monitor with mainMod + [F1-7] bind = $mainMod, F1, workspace, 11 bind = $mainMod, F2, workspace, 12 bind = $mainMod, F3, workspace, 13 @@ -268,9 +278,8 @@ bind = $mainMod, F4, workspace, 14 bind = $mainMod, F5, workspace, 15 bind = $mainMod, F6, workspace, 16 bind = $mainMod, F7, workspace, 17 -bind = $mainMod, F8, workspace, 18 -# Move active window to a workspace with mainMod + SHIFT + [1-9] (primary monitor) +# Move active window to a workspace with mainMod + SHIFT + [1-7] (primary monitor) bind = $mainMod SHIFT, 1, movetoworkspace, 1 bind = $mainMod SHIFT, 2, movetoworkspace, 2 bind = $mainMod SHIFT, 3, movetoworkspace, 3 @@ -279,7 +288,7 @@ bind = $mainMod SHIFT, 5, movetoworkspace, 5 bind = $mainMod SHIFT, 6, movetoworkspace, 6 bind = $mainMod SHIFT, 7, movetoworkspace, 7 -# Move active window to a workspace with mainMod + SHIFT + [F1-F9] (secondary monitor) +# Move active window to a workspace with mainMod + SHIFT + [F1-F7] (secondary monitor) bind = $mainMod SHIFT, F1, movetoworkspace, 11 bind = $mainMod SHIFT, F2, movetoworkspace, 12 bind = $mainMod SHIFT, F3, movetoworkspace, 13 @@ -289,14 +298,20 @@ bind = $mainMod SHIFT, F6, movetoworkspace, 16 bind = $mainMod SHIFT, F7, movetoworkspace, 17 # Special workspaces (scratchpad) +# Special workspaces should only be opened on the primary monitor. +bind = $mainMod, F12, focusmonitor, DP-1 bind = $mainMod, F12, togglespecialworkspace, term bind = $mainMod SHIFT, F12, movetoworkspace, special:term +bind = $mainMod, F11, focusmonitor, DP-1 bind = $mainMod, F11, togglespecialworkspace, browser bind = $mainMod SHIFT, F11, movetoworkspace, special:browser +bind = $mainMod, F10, focusmonitor, DP-1 bind = $mainMod, F10, togglespecialworkspace, music bind = $mainMod SHIFT, F10, movetoworkspace, special:music +bind = $mainMod, F9, focusmonitor, DP-1 bind = $mainMod, F9, togglespecialworkspace, todo bind = $mainMod SHIFT, F9, movetoworkspace, special:todo +bind = $mainMod, F8, focusmonitor, DP-1 bind = $mainMod, F8, togglespecialworkspace, files bind = $mainMod SHIFT, F8, movetoworkspace, special:files @@ -314,8 +329,8 @@ bindel = ,XF86MonBrightnessDown, exec, brightnessctl s 10%- # Requires playerctl bindl = , XF86AudioNext, exec, playerctl next -bindl = , XF86AudioPause, exec, playerctl play-pause -bindl = , XF86AudioPlay, exec, playerctl play-pause +bindl = , XF86AudioPause, exec, playerctl play-pause -a +bindl = , XF86AudioPlay, exec, playerctl play-pause -a bindl = , XF86AudioPrev, exec, playerctl previous ############################## @@ -326,49 +341,103 @@ bindl = , XF86AudioPrev, exec, playerctl previous # See https://wiki.hyprland.org/Configuring/Workspace-Rules/ for workspace rules # Ignore maximize requests from apps. You'll probably like this. -windowrulev2 = suppressevent maximize, class:.* +windowrule = match:class .*, suppress_event maximize # Fix some dragging issues with XWayland -windowrulev2 = nofocus,class:^$,title:^$,xwayland:1,floating:1,fullscreen:0,pinned:0 +# windowrulev2 = nofocus,class:^$,title:^$,xwayland:1,floating:1,fullscreen:0,pinned:0 # Window rules to assign windows to workspaces -windowrulev2 = float,title:^(.*Extension: Bitwarden.*)$ +windowrule = match:title ^(.*Extension: Bitwarden.*)$, float 1 # Always center ClickUp command bar -windowrulev2 = center, title:ClickUp Command Bar +windowrule = match:title ClickUp Command Bar, center 1 # Dont use floating mode for freerdp -windowrulev2 = tile, class:xfreerdp +windowrule = match:class ^(xfreerdp)$, tile 1 # Open audio settings in floating mode -windowrulev2 = float, class:org.pulseaudio.pavucontrol -windowrulev2 = size 500 1000, class:org.pulseaudio.pavucontrol -windowrulev2 = move 2841 61, class:org.pulseaudio.pavucontrol -windowrulev2 = pin, class:org.pulseaudio.pavucontrol +windowrule = match:class org.pulseaudio.pavucontrol, float 1, size 500 1000, move 2841 61, pin 1 # Same for bluetooth settings -windowrulev2 = float, initialTitle:overskride -windowrulev2 = size 765 765, initialTitle:overskride -windowrulev2 = move 2653 44, initialTitle:overskride -windowrulev2 = pin, initialTitle:overskride +windowrule = match:title overskride, float 1, size 765 765, move 2653 44, pin 1 # Change border color of pinned windows -windowrulev2 = bordercolor rgb(CC34A4) rgb(4B2D70), pinned:1 +windowrule = match:pin 1, border_color rgb(CC34A4) rgb(4B2D70) + +# Share Picker is always floating +windowrule = match:class com.paulaumann.hyprland-share-picker, float 1, center 1 + +# Waydroid is always floating and exact size +windowrule { + name = "waydroid" + match:initial_class = Waydroid + + float = 1 + size = 576 1024 +} + +# Vicinae is always floating and exact size +windowrule { + name = "vicinae" + match:initial_class = vicinae-server + float = 1 + pin = 1 + stay_focused = on + dim_around = on + no_anim = on +} # Notification animation style -layerrule = animation slide, notifications +layerrule = match:namespace notifications, animation slide + +# Fixes black border around screenshots due to animation after selection +layerrule { + name = no_anim_for_selection + match:namespace = selection + no_anim = on +} + +# Blur behind certain layers +layerrule { + name = blur_waybar + match:namespace = waybar + blur = on +} + +layerrule { + name = blur_tofi + match:namespace = launcher + blur = on +} + +# layerrule { +# name = blur_vicinae +# match:namespace = vicinae +# blur = on +# ignore_alpha = on +# dim_around = on +# } + +# layerrule { +# name = no_animation_for_vicinae +# match:namespace = vicinae +# no_anim = on +# } # Workspace rules to assign workspaces to monitors -workspace = 1, monitor:DP-2, default:true -workspace = 2, monitor:DP-2 -workspace = 3, monitor:DP-2 -workspace = 4, monitor:DP-2 -workspace = 5, monitor:DP-2 -workspace = 11, monitor:DP-3, default:true -workspace = 12, monitor:DP-3 -workspace = 13, monitor:DP-3 -workspace = 14, monitor:DP-3 -workspace = 15, monitor:DP-3 +workspace = 1, monitor:DP-1, default:true +workspace = 2, monitor:DP-1 +workspace = 3, monitor:DP-1 +workspace = 4, monitor:DP-1 +workspace = 5, monitor:DP-1 +workspace = 11, monitor:DP-2, default:true +workspace = 12, monitor:DP-2 +workspace = 13, monitor:DP-2 +workspace = 14, monitor:DP-2 +workspace = 15, monitor:DP-2 -workspace = special:music, gapsout:40 1800 50 50 -workspace = special:music, monitor:DP-2 +workspace = special:term, gapsout:64 +workspace = special:browser, gapsout:64 +workspace = special:music, gapsout:64 +workspace = special:todo, gapsout:64 +workspace = special:files, gapsout:64 \ No newline at end of file diff --git a/.config/hypr/hyprpaper.conf b/.config/hypr/hyprpaper.conf index d1f5dbb..978c1e6 100644 --- a/.config/hypr/hyprpaper.conf +++ b/.config/hypr/hyprpaper.conf @@ -1,2 +1,13 @@ -preload = ~/.config/hypr/img/gnome-background.webp -wallpaper = , ~/.config/hypr/img/gnome-background.webp \ No newline at end of file +wallpaper { + monitor = DP-1 + path = ~/.config/hypr/img/gnome-background.webp + fit_mode = cover +} + +wallpaper { + monitor = DP-2 + path = ~/.config/hypr/img/gnome-background.webp + fit_mode = cover +} + +splash = false \ No newline at end of file diff --git a/.config/settings.json b/.config/settings.json new file mode 120000 index 0000000..0f40aca --- /dev/null +++ b/.config/settings.json @@ -0,0 +1 @@ +vicinae/settings.json \ No newline at end of file diff --git a/.config/spotify-player/app.toml b/.config/spotify-player/app.toml new file mode 100644 index 0000000..3530ab5 --- /dev/null +++ b/.config/spotify-player/app.toml @@ -0,0 +1,51 @@ +theme = "dracula" +client_id = "7fed45e8d6ec4ff1b50e5464852fe136" +client_port = 8080 +login_redirect_uri = "http://127.0.0.1:8989/login" +playback_format = """ +{status} {track} • {artists} {liked} +{album} +{metadata}""" +notify_timeout_in_secs = 0 +tracks_playback_limit = 50 +app_refresh_duration_in_ms = 32 +playback_refresh_duration_in_ms = 0 +page_size_in_rows = 20 +play_icon = "▶" +pause_icon = "▌▌" +liked_icon = "♥" +border_type = "Plain" +progress_bar_type = "Rectangle" +cover_img_length = 9 +cover_img_width = 5 +cover_img_scale = 1.0 +cover_img_pixels = 64 +enable_media_control = true +enable_streaming = "Always" +enable_notify = true +enable_cover_image_cache = true +default_device = "spotify-player" +notify_streaming_only = false +seek_duration_secs = 5 +sort_artist_albums_by_type = false + +[notify_format] +summary = "{track} • {artists}" +body = "{album}" + +[layout] +playback_window_position = "Top" +playback_window_height = 6 + +[layout.library] +playlist_percent = 40 +album_percent = 40 + +[device] +name = "spotify-player" +device_type = "speaker" +volume = 70 +bitrate = 320 +audio_cache = false +normalization = false +autoplay = false diff --git a/.config/vicinae/.vscode/settings.json b/.config/vicinae/.vscode/settings.json new file mode 100644 index 0000000..38b6504 --- /dev/null +++ b/.config/vicinae/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "[json]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" + } + }, + "[jsonc]": { + "editor.formatOnSave": true + }, +} \ No newline at end of file diff --git a/.config/vicinae/_default.jsonc b/.config/vicinae/_default.jsonc new file mode 100644 index 0000000..c4bac90 --- /dev/null +++ b/.config/vicinae/_default.jsonc @@ -0,0 +1,186 @@ +{ + "$schema": "https://vicinae.com/schemas/config.json", + + // A list of paths pointed to separate configuration files to source during loading. + // Note that values defined in the main configuration file always take precedence over imported + // files. + // Relative paths are accepted, and are relative to the configuration directory in which the importing configuration file is located. + "imports": [], + + // Whether root search should also search files directly. + // File search is performed asynchrounously: files may take an instant to appear after a query is entered. + // Turning this on results in an increase in CPU usage when searching. + // Note that vicinae has a built-in file search command that can be used to perform file search in isolation, accompanied with + // rich content preview. This should be more than enough for most users. + "search_files_in_root": false, + + // What vicinae should do when the "escape" key is pressed + // Supports "navigate_back" or "close_window" + "escape_key_behavior": "navigate_back", + + // If the layer shell protocol is used to position the window and the keyboard interactivity is + // set to "exclusive" (the default) then this setting will have no effect. Either switch to "on_demand" interactivity + // or disable layer shell. + "close_on_focus_loss": false, + + // Whether IME preedit strings should be searched in real time. + "consider_preedit": false, + + // Reset the navigation state every time the window is closed + "pop_to_root_on_close": false, + + // What favicon service to use when loading favicons is needed. + // Available values are: 'twenty' | 'google' | 'none' + // If this is set to 'none', favicon loading is disabled and a placeholder icon will be used when a favicon is expected. + "favicon_service": "twenty", + + // EXPERIMENTAL! + // Enable specific editing motions in the main search bar and during navigation. + // This will probably be changed in future updates. + // Supports: 'default' | 'emacs' + "keybinding": "default", + + "font": { + // The font family to use for the general vicinae UI. + // "auto" (recommended): uses the bundled Inter font + // "system": uses the system/platform default font + // Or specify any font family name (e.g. "Fira Sans") + "normal": { + "family": "auto", + // The point size of the font (unlike pixel size, the point size scales with resolution) + "size": 10.5 + } + }, + + // The general vicinae theme as well as the system icon theme (used for applications and file icons) can be customized + // according to the system appearance. + // When editing the current theme through the vicinae GUI, it will modify the value that maps to the current system appearance. + "theme": { + "light": { + "name": "vicinae-light", + "icon_theme": "auto" + }, + "dark": { + "name": "vicinae-dark", + "icon_theme": "auto" + } + }, + + "launcher_window": { + // Control the opacity of the main window. + // Opacity for other surfaces are controlled by the active theme. + "opacity": 0.95, + + // Blur the window background. + // Only suported on Hyprland for now. + // May require a server restart to disable properly. + "blur": { + "enabled": true + }, + + // Dims everything behind the vicinae window. + // Only supported on Hyprland for now. + // May require a server restart to disable properly. + "dim_around": true, + + // You can turn client side decorations off if you want to let your compositor draw the rounded + // borders and such. This usually gives better results, but is more complicated to setup, if at all possible. + "client_side_decorations": { + "enabled": true, + "rounding": 10, + "border_width": 2 + }, + + // In compact mode, vicinae only shows a search bar in the root search if no query is entered, only expanding to its full size when searching. + // WARNING: compact mode works best when vicinae is rendered as a layer surface (the default if available), as opposed to a regular floating window. + // That's because the part that is not rendered in compact mode is still part of the full window size, allowing the window to be gracefully expanded without having to deal + // with a compositor window resize which can generate a lot of visual noise. Server side borders and blur will look notably out of place. + "compact_mode": { + "enabled": false + }, + + // The vicinae UI is designed to work best at the default size. If you change this it is highly recommended + // that you preserve a similar aspect ratio. + "size": { + "width": 770, + "height": 480 + }, + + // EXPERIMENTAL - X11 ONLY! + // The name of the screen (as provided by a tool like xrandr) on which the vicinae window + // needs to be shown. + // e.g "eDP1", "DP-1" + "screen": "auto", + + // Only for wayland compositors that support the 'wlr-layer-shell' protocol (https://wayland.app/protocols/wlr-layer-shell-unstable-v1) + // NOTE: layer shell support for the cosmic compositor is explicitly disabled as it is currently broken + "layer_shell": { + "enabled": true, + + // 'exclusive' | 'on_demand' + // WARNING: 'exclusive' is known to break mouse stuff on popups (such as the action panel) on Hyprland + "keyboard_interactivity": "on_demand", + + // either 'overlay' or 'top'. + // Other layers are not supported as they are unsuitable for a launcher. + // 'top' is recommended as using 'overlay' will make the vicinae window appear on top of IME popovers in some scenarios. + "layer": "top" + } + }, + + // How much memory (in MB) can be used to cache small image assets in memory directly + // The higher this value is the more memory vicinae will use. In exchange for this, you get lower cpu usage. + "pixmap_cache_mb": 50, + + // Keybinds are serialized using a custom format. + // It is recommended to edit them through the settings GUI, which will write them to this file. + "keybinds": { + // common shortcuts + "open-search-filter": "control+P", + "open-settings": "control+,", + "toggle-action-panel": "control+B", + + // used to assign shortcuts to generic actions + // if an extension provides a specific kind of action, it is encouraged to use shortcuts as defined here. + "action.copy": "control+shift+C", + "action.copy-name": "control+shift+.", + "action.copy-path": "control+shift+,", + "action.dangerous-remove": "control+shift+X", + "action.duplicate": "control+D", + "action.edit": "control+E", + "action.edit-secondary": "control+shift+E", + "action.move-down": "control+shift+ARROWDOWN", + "action.move-up": "control+shift+ARROWUP", + "action.new": "control+N", + "action.open": "control+O", + "action.pin": "control+shift+P", + "action.refresh": "control+R", + "action.remove": "control+X", + "action.save": "control+S" + }, + + // List of entrypoints that are tagged as "favorite". + // They show up on the very top of the root search when no search query is active. + // Each value is a serialized entrypoint ID. + "favorites": [ + "clipboard:history" + ], + + // List of entrypoints to suggest as a fallback when no result matches the + // provided search query. + // Each value is a serialized entrypoint ID. + "fallbacks": [ + "files:search" + ], + + // Every item that can be activated from the vicinae root search is referred as an entrypoint, or sometimes, in the case of extensions, as a "command". + // Entrypoints are always grouped under a provider. + // e,g applications entrypoints are all grouped under the "applications" provider + // + // The best way to interact with providers and entrypoints is through the graphical interface provided by the + // settings window ("extensions" tab). Every time a change is made through this GUI, the relevant values will be + // written to the "providers" object below (password preferences excluded). + // + // The exact list of available providers depends on what extensions have been installed. + "providers": {} +} diff --git a/.config/vicinae/appeareance.jsonc b/.config/vicinae/appeareance.jsonc new file mode 100644 index 0000000..e7b2432 --- /dev/null +++ b/.config/vicinae/appeareance.jsonc @@ -0,0 +1,22 @@ +{ + "$schema": "https://vicinae.com/schemas/config.json", + "launcher_window": { + // You can turn client side decorations off if you want to let your compositor draw the rounded + // borders and such. This usually gives better results, but is more complicated to setup, if at all possible. + "client_side_decorations": { + "enabled": false + }, + // Only for wayland compositors that support the 'wlr-layer-shell' protocol (https://wayland.app/protocols/wlr-layer-shell-unstable-v1) + // NOTE: layer shell support for the cosmic compositor is explicitly disabled as it is currently broken + "layer_shell": { + "enabled": false, + // 'exclusive' | 'on_demand' + // WARNING: 'exclusive' is known to break mouse stuff on popups (such as the action panel) on Hyprland + "keyboard_interactivity": "on_demand", + // either 'overlay' or 'top'. + // Other layers are not supported as they are unsuitable for a launcher. + // 'top' is recommended as using 'overlay' will make the vicinae window appear on top of IME popovers in some scenarios. + "layer": "top" + } + } +} \ No newline at end of file diff --git a/.config/vicinae/general.jsonc b/.config/vicinae/general.jsonc new file mode 100644 index 0000000..3f9d891 --- /dev/null +++ b/.config/vicinae/general.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "https://vicinae.com/schemas/config.json", + // If the layer shell protocol is used to position the window and the keyboard interactivity is + // set to "exclusive" (the default) then this setting will have no effect. Either switch to "on_demand" interactivity + // or disable layer shell. + "close_on_focus_loss": false +} \ No newline at end of file diff --git a/.config/vicinae/settings.json b/.config/vicinae/settings.json new file mode 100644 index 0000000..73fb94d --- /dev/null +++ b/.config/vicinae/settings.json @@ -0,0 +1,17 @@ +// This configuration is merged with the default vicinae configuration file, which you can obtain by running the `vicinae config default` command. +// Every item defined in this file takes precedence over the values defined in the default config or any other imported file. +// +// You can make manual edits to this file, however you should keep in mind that this file may be written to by vicinae when a configuration change is made through the GUI. +// When that happens, any custom comments or formatting will be lost. +// +// If you want to maintain a configuration file with your own comments and formatting, you should create a separate file and add it to the 'imports' array. +// +// Learn more about configuration at https://docs.vicinae.com/config + +{ + "$schema": "https://vicinae.com/schemas/config.json", + "imports": [ + "appeareance.jsonc", + "general.jsonc" + ] +} \ No newline at end of file diff --git a/.config/waybar/config.jsonc b/.config/waybar/config.jsonc index 97cf25f..570d31d 100644 --- a/.config/waybar/config.jsonc +++ b/.config/waybar/config.jsonc @@ -1,6 +1,6 @@ { "layer": "top", - "output": "DP-2", + "output": "DP-1", "modules-left": [ "custom/power", "hyprland/workspaces", diff --git a/.config/winapps/compose.yaml b/.config/winapps/compose.yaml new file mode 100644 index 0000000..21780dc --- /dev/null +++ b/.config/winapps/compose.yaml @@ -0,0 +1,51 @@ +# For documentation, FAQ, additional configuration options and technical help, visit: https://github.com/dockur/windows + +name: "winapps" # Docker Compose Project Name. +# volumes: + # Create Volume 'data'. + # Located @ '/var/lib/docker/volumes/winapps_data/_data' (Docker). + # Located @ '/var/lib/containers/storage/volumes/winapps_data/_data' or '~/.local/share/containers/storage/volumes/winapps_data/_data' (Podman). + # data: + +services: + windows: + image: ghcr.io/dockur/windows:latest + container_name: winapps # Created Docker VM Name. + environment: + # Version of Windows to configure. For valid options, visit: + # https://github.com/dockur/windows?tab=readme-ov-file#how-do-i-select-the-windows-version + # https://github.com/dockur/windows?tab=readme-ov-file#how-do-i-install-a-custom-image + VERSION: "11" + RAM_SIZE: "8G" # RAM allocated to the Windows VM. + CPU_CORES: "4" # CPU cores allocated to the Windows VM. + DISK_SIZE: "64G" # Size of the primary hard disk. + # DISK2_SIZE: "32G" # Uncomment to add an additional hard disk to the Windows VM. Ensure it is mounted as a volume below. + USERNAME: "paul" # Edit here to set a custom Windows username. The default is 'MyWindowsUser'. + PASSWORD: "nicerdicer" # Edit here to set a password for the Windows user. The default is 'MyWindowsPassword'. + HOME: "${HOME}" # Set path to Linux user home folder. + ARGUMENTS: "-cpu host,arch_capabilities=off" + ports: + - 8006:8006 # Map '8006' on Linux host to '8006' on Windows VM --> For VNC Web Interface @ http://127.0.0.1:8006. + - 3389:3389/tcp # Map '3389' on Linux host to '3389' on Windows VM --> For Remote Desktop Protocol (RDP). + - 3389:3389/udp # Map '3389' on Linux host to '3389' on Windows VM --> For Remote Desktop Protocol (RDP). + cap_add: + - NET_ADMIN # Add network permission + stop_grace_period: 120s # Wait 120 seconds before sending SIGTERM when attempting to shut down the Windows VM. + restart: on-failure # Restart the Windows VM if the exit code indicates an error. + volumes: + - ~/.winapps:/storage # Mount local folder 'data' to use as Windows 'C:' drive. + - ${HOME}:/shared # Mount Linux user home directory @ '\\host.lan\Data'. + #- /path/to/second/hard/disk:/storage2 # Uncomment to create a virtual second hard disk and mount it within the Windows VM. Ensure 'DISK2_SIZE' is specified above. + - ./oem:/oem # Enables automatic post-install execution of 'oem/install.bat', applying Windows registry modifications contained within 'oem/RDPApps.reg'. + #- /path/to/windows/install/media.iso:/custom.iso # Uncomment to use a custom Windows ISO. If specified, 'VERSION' (e.g. 'tiny11') will be ignored. + devices: + - /dev/kvm # Enable KVM. + - /dev/net/tun # Enable tuntap + # Uncomment to mount a disk directly within the Windows VM. + # WARNING: /dev/sdX paths may change after reboot. Use persistent identifiers! + # NOTE: 'disk1' will be mounted as the main drive. THIS DISK WILL BE FORMATTED BY DOCKER. + # All following disks (disk2, ...) WILL NOT BE FORMATTED. + # - /dev/disk/by-id/:/disk1 + # - /dev/disk/by-id/:/disk2 + # group_add: # uncomment this line and the next one for using rootless podman containers + # - keep-groups # to make /dev/kvm work with podman. needs "crun" installed, "runc" will not work! Add your user to the 'kvm' group or another that can access /dev/kvm. diff --git a/.config/winapps/setup.sh b/.config/winapps/setup.sh new file mode 100755 index 0000000..65648b1 --- /dev/null +++ b/.config/winapps/setup.sh @@ -0,0 +1,1794 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC2034 # Silence warnings regarding unused variables globally. + +### GLOBAL CONSTANTS ### +# ANSI ESCAPE SEQUENCES +readonly BOLD_TEXT="\033[1m" # Bold +readonly CLEAR_TEXT="\033[0m" # Clear +readonly COMMAND_TEXT="\033[0;37m" # Grey +readonly DONE_TEXT="\033[0;32m" # Green +readonly ERROR_TEXT="\033[1;31m" # Bold + Red +readonly EXIT_TEXT="\033[1;41;37m" # Bold + White + Red Background +readonly FAIL_TEXT="\033[0;91m" # Bright Red +readonly INFO_TEXT="\033[0;33m" # Orange/Yellow +readonly SUCCESS_TEXT="\033[1;42;37m" # Bold + White + Green Background +readonly WARNING_TEXT="\033[1;33m" # Bold + Orange/Yellow + +# ERROR CODES +readonly EC_FAILED_CD="1" # Failed to change directory to location of script. +readonly EC_BAD_ARGUMENT="2" # Unsupported argument passed to script. +readonly EC_EXISTING_INSTALL="3" # Existing conflicting WinApps installation. +readonly EC_NO_CONFIG="4" # Absence of a valid WinApps configuration file. +readonly EC_MISSING_DEPS="5" # Missing dependencies. +readonly EC_NO_SUDO="6" # Insufficient privileges to invoke superuser access. +readonly EC_NOT_IN_GROUP="7" # Current user not in group 'libvirt' and/or 'kvm'. +readonly EC_VM_OFF="8" # Windows 'libvirt' VM powered off. +readonly EC_VM_PAUSED="9" # Windows 'libvirt' VM paused. +readonly EC_VM_ABSENT="10" # Windows 'libvirt' VM does not exist. +readonly EC_CONTAINER_OFF="11" # Windows Docker container is not running. +readonly EC_NO_IP="12" # Windows does not have an IP address. +readonly EC_BAD_PORT="13" # Windows is unreachable via RDP_PORT. +readonly EC_RDP_FAIL="14" # FreeRDP failed to establish a connection with Windows. +readonly EC_APPQUERY_FAIL="15" # Failed to query Windows for installed applications. +readonly EC_INVALID_FLAVOR="16" # Backend specified is not 'libvirt', 'docker' or 'podman'. + +# PATHS +# 'BIN' +readonly SYS_BIN_PATH="/usr/local/bin" # UNIX path to 'bin' directory for a '--system' WinApps installation. +readonly USER_BIN_PATH="${HOME}/.local/bin" # UNIX path to 'bin' directory for a '--user' WinApps installation. +readonly USER_BIN_PATH_WIN='\\tsclient\home\.local\bin' # WINDOWS path to 'bin' directory for a '--user' WinApps installation. +# 'SOURCE' +readonly SYS_SOURCE_PATH="${SYS_BIN_PATH}/winapps-src" # UNIX path to WinApps source directory for a '--system' WinApps installation. +readonly USER_SOURCE_PATH="${USER_BIN_PATH}/winapps-src" # UNIX path to WinApps source directory for a '--user' WinApps installation. +# 'APP' +readonly SYS_APP_PATH="/usr/share/applications" # UNIX path to 'applications' directory for a '--system' WinApps installation. +readonly USER_APP_PATH="${HOME}/.local/share/applications" # UNIX path to 'applications' directory for a '--user' WinApps installation. +readonly USER_APP_PATH_WIN='\\tsclient\home\.local\share\applications' # WINDOWS path to 'applications' directory for a '--user' WinApps installation. +# 'APPDATA' +readonly SYS_APPDATA_PATH="/usr/local/share/winapps" # UNIX path to 'application data' directory for a '--system' WinApps installation. +readonly USER_APPDATA_PATH="${HOME}/.local/share/winapps" # UNIX path to 'application data' directory for a '--user' WinApps installation. +readonly USER_APPDATA_PATH_WIN='\\tsclient\home\.local\share\winapps' # WINDOWS path to 'application data' directory for a '--user' WinApps installation. +# 'Installed Batch Script' +readonly BATCH_SCRIPT_PATH="${USER_APPDATA_PATH}/installed.bat" # UNIX path to a batch script used to search Windows for applications. +readonly BATCH_SCRIPT_PATH_WIN="${USER_APPDATA_PATH_WIN}\\installed.bat" # WINDOWS path to a batch script used to search Windows for applications. +# 'Installed File' +readonly TMP_INST_FILE_PATH="${USER_APPDATA_PATH}/installed.tmp" # UNIX path to a temporary file containing the names of detected officially supported applications. +readonly TMP_INST_FILE_PATH_WIN="${USER_APPDATA_PATH_WIN}\\installed.tmp" # WINDOWS path to a temporary file containing the names of detected officially supported applications. +readonly INST_FILE_PATH="${USER_APPDATA_PATH}/installed" # UNIX path to a file containing the names of detected officially supported applications. +readonly INST_FILE_PATH_WIN="${USER_APPDATA_PATH_WIN}\\installed" # WINDOWS path to a file containing the names of detected officially supported applications. +# 'PowerShell Script' +readonly PS_SCRIPT_PATH="./install/ExtractPrograms.ps1" # UNIX path to a PowerShell script used to store the names, executable paths and icons (base64) of detected applications. +readonly PS_SCRIPT_HOME_PATH="${USER_APPDATA_PATH}/ExtractPrograms.ps1" # UNIX path to a copy of the PowerShell script within the user's home directory to enable access by Windows. +readonly PS_SCRIPT_HOME_PATH_WIN="${USER_APPDATA_PATH_WIN}\\ExtractPrograms.ps1" # WINDOWS path to a copy of the PowerShell script within the user's home directory to enable access by Windows. +# 'Detected File' +readonly DETECTED_FILE_PATH="${USER_APPDATA_PATH}/detected" # UNIX path to a file containing the output generated by the PowerShell script, formatted to define bash arrays. +readonly DETECTED_FILE_PATH_WIN="${USER_APPDATA_PATH_WIN}\\detected" # WINDOWS path to a file containing the output generated by the PowerShell script, formatted to define bash arrays. +# 'FreeRDP Connection Test File' +readonly TEST_PATH="${USER_APPDATA_PATH}/FreeRDP_Connection_Test" # UNIX path to temporary file whose existence is used to confirm a successful RDP connection was established. +readonly TEST_PATH_WIN="${USER_APPDATA_PATH_WIN}\\FreeRDP_Connection_Test" # WINDOWS path to temporary file whose existence is used to confirm a successful RDP connection was established. +# 'WinApps Configuration File' +readonly CONFIG_PATH="${HOME}/.config/winapps/winapps.conf" # UNIX path to the WinApps configuration file. +# 'Inquirer Bash Script' +readonly INQUIRER_PATH="./install/inquirer.sh" # UNIX path to the 'inquirer' script, which is used to produce selection menus. + +# REMOTE DESKTOP CONFIGURATION +readonly RDP_PORT=3389 # Port used for RDP on Windows. +readonly DOCKER_IP="127.0.0.1" # Localhost. + +### GLOBAL VARIABLES ### +# USER INPUT +OPT_SYSTEM=0 # Set to '1' if the user specifies '--system'. +OPT_USER=0 # Set to '1' if the user specifies '--user'. +OPT_UNINSTALL=0 # Set to '1' if the user specifies '--uninstall'. +OPT_AOSA=0 # Set to '1' if the user specifies '--setupAllOfficiallySupportedApps'. + +# WINAPPS CONFIGURATION FILE +RDP_USER="" # Imported variable. +RDP_PASS="" # Imported variable. +RDP_DOMAIN="" # Imported variable. +RDP_IP="" # Imported variable. +VM_NAME="RDPWindows" # Name of the Windows VM (FOR 'libvirt' ONLY). +WAFLAVOR="docker" # Imported variable. +RDP_SCALE=100 # Imported variable. +RDP_FLAGS="" # Imported variable. +DEBUG="true" # Imported variable. +FREERDP_COMMAND="" # Imported variable. + +PORT_TIMEOUT=5 # Default port check timeout. +RDP_TIMEOUT=30 # Default RDP connection test timeout. +APP_SCAN_TIMEOUT=60 # Default application scan timeout. + +# PERMISSIONS AND DIRECTORIES +SUDO="" # Set to "sudo" if the user specifies '--system', or "" if the user specifies '--user'. +BIN_PATH="" # Set to $SYS_BIN_PATH if the user specifies '--system', or $USER_BIN_PATH if the user specifies '--user'. +APP_PATH="" # Set to $SYS_APP_PATH if the user specifies '--system', or $USER_APP_PATH if the user specifies '--user'. +APPDATA_PATH="" # Set to $SYS_APPDATA_PATH if the user specifies '--system', or $USER_APPDATA_PATH if the user specifies '--user'. +SOURCE_PATH="" # Set to $SYS_SOURCE_PATH if the user specifies '--system', or $USER_SOURCE_PATH if the user specifies '--user'. + +# INSTALLATION PROCESS +INSTALLED_EXES=() # List of executable file names of officially supported applications that have already been configured during the current installation process. + +### TRAPS ### +set -o errtrace # Ensure traps are inherited by all shell functions and subshells. +trap "waTerminateScript" ERR # Catch non-zero return values. + +### FUNCTIONS ### +# Name: 'waTerminateScript' +# Role: Terminates the script when a non-zero return value is encountered. +# shellcheck disable=SC2317 # Silence warning regarding this function being unreachable. +function waTerminateScript() { + # Store the non-zero exit status received by the trap. + local EXIT_STATUS=$? + + # Display the exit status. + echo -e "${EXIT_TEXT}Exiting with status '${EXIT_STATUS}'.${CLEAR_TEXT}" + + # Terminate the script. + exit "$EXIT_STATUS" +} + +# Name: 'waUsage' +# Role: Displays usage information for the script. +function waUsage() { + echo -e "Usage: + ${COMMAND_TEXT} --user${CLEAR_TEXT} # Install WinApps and selected applications in ${HOME} + ${COMMAND_TEXT} --system${CLEAR_TEXT} # Install WinApps and selected applications in /usr + ${COMMAND_TEXT} --user --setupAllOfficiallySupportedApps${CLEAR_TEXT} # Install WinApps and all officially supported applications in ${HOME} + ${COMMAND_TEXT} --system --setupAllOfficiallySupportedApps${CLEAR_TEXT} # Install WinApps and all officially supported applications in /usr + ${COMMAND_TEXT} --user --uninstall${CLEAR_TEXT} # Uninstall everything in ${HOME} + ${COMMAND_TEXT} --system --uninstall${CLEAR_TEXT} # Uninstall everything in /usr + ${COMMAND_TEXT} --help${CLEAR_TEXT} # Display this usage message." +} + +# Name: 'waGetSourceCode' +# Role: Grab the WinApps source code using Git. +function waGetSourceCode() { + # Declare variables. + local SCRIPT_DIR_PATH="" # Stores the absolute path of the directory containing the script. + + # Determine the absolute path to the directory containing the script. + SCRIPT_DIR_PATH=$(readlink -f "$(dirname "${BASH_SOURCE[0]}")") + + # Check if winapps is currently installed on $SOURCE_PATH + if [[ -f "$SCRIPT_DIR_PATH/winapps" && "$SCRIPT_DIR_PATH" != "$SOURCE_PATH" ]]; then + # Display a warning. + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} You are running a WinApps installation located outside of default location '${SOURCE_PATH}'. A new installation will be created." + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} You might want to remove your old installation on '${SCRIPT_DIR_PATH}'." + fi + + if [[ ! -d "$SOURCE_PATH" ]]; then + $SUDO git clone --recurse-submodules --remote-submodules https://github.com/winapps-org/winapps.git "$SOURCE_PATH" + else + echo -e "${INFO_TEXT}WinApps installation already present at ${CLEAR_TEXT}${COMMAND_TEXT}${SOURCE_PATH}${CLEAR_TEXT}${INFO_TEXT}. Updating...${CLEAR_TEXT}" + $SUDO git -C "$SOURCE_PATH" pull --no-rebase + fi + + # Silently change the working directory. + if ! cd "$SOURCE_PATH" &>/dev/null; then + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}DIRECTORY CHANGE FAILURE.${CLEAR_TEXT}" + + # Display error details. + echo -e "${INFO_TEXT}Failed to change the working directory to ${CLEAR_TEXT}${COMMAND_TEXT}${SOURCE_PATH}${CLEAR_TEXT}${INFO_TEXT}.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Ensure:" + echo -e " - ${COMMAND_TEXT}${SOURCE_PATH}${CLEAR_TEXT} exists." + echo -e " - ${COMMAND_TEXT}${SOURCE_PATH}${CLEAR_TEXT} has been cloned and checked out properly." + echo -e " - The current user has sufficient permissions to access and write to ${COMMAND_TEXT}${SOURCE_PATH}${CLEAR_TEXT}." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_FAILED_CD" + fi +} + +# Name: 'waGetInquirer' +# Role: Loads the inquirer script, even if the source isn't cloned yet +function waGetInquirer() { + local INQUIRER=$INQUIRER_PATH + + if [ -d "$SYS_SOURCE_PATH" ]; then + INQUIRER=$SYS_SOURCE_PATH/$INQUIRER_PATH + elif [ -d "$USER_SOURCE_PATH" ] ; then + INQUIRER=$USER_SOURCE_PATH/$INQUIRER_PATH + else + INQUIRER="/tmp/waInquirer.sh" + rm -f "$INQUIRER" + + curl -o "$INQUIRER" "https://raw.githubusercontent.com/winapps-org/winapps/main/install/inquirer.sh" + fi + + # shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck. + source "$INQUIRER" +} + +# Name: 'waCheckInput' +# Role: Sanitises input and guides users through selecting appropriate options if no arguments are provided. +function waCheckInput() { + # Declare variables. + local OPTIONS=() # Stores the options. + local SELECTED_OPTION # Stores the option selected by the user. + + if [[ $# -gt 0 ]]; then + # Parse arguments. + for argument in "$@"; do + case "$argument" in + "--user") + OPT_USER=1 + ;; + "--system") + OPT_SYSTEM=1 + ;; + "--setupAllOfficiallySupportedApps") + OPT_AOSA=1 + ;; + "--uninstall") + OPT_UNINSTALL=1 + ;; + "--help") + waUsage + exit 0 + ;; + *) + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}INVALID ARGUMENT.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}Unsupported argument${CLEAR_TEXT} ${COMMAND_TEXT}${argument}${CLEAR_TEXT}${INFO_TEXT}.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + waUsage + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_BAD_ARGUMENT" + ;; + esac + done + else + # Install vs. uninstall? + OPTIONS=("Install" "Uninstall") + inqMenu "Install or uninstall WinApps?" OPTIONS SELECTED_OPTION + + # Set flags. + if [[ $SELECTED_OPTION == "Uninstall" ]]; then + OPT_UNINSTALL=1 + fi + + # User vs. system? + OPTIONS=("Current User" "System") + inqMenu "Configure WinApps for the current user '$(whoami)' or the whole system?" OPTIONS SELECTED_OPTION + + # Set flags. + if [[ $SELECTED_OPTION == "Current User" ]]; then + OPT_USER=1 + elif [[ $SELECTED_OPTION == "System" ]]; then + OPT_SYSTEM=1 + fi + + # Automatic vs. manual? + if [ "$OPT_UNINSTALL" -eq 0 ]; then + OPTIONS=("Manual (Default)" "Automatic") + inqMenu "Automatically install supported applications or choose manually?" OPTIONS SELECTED_OPTION + + # Set flags. + if [[ $SELECTED_OPTION == "Automatic" ]]; then + OPT_AOSA=1 + fi + fi + + # Newline. + echo "" + fi + + # Simultaneous 'User' and 'System'. + if [ "$OPT_SYSTEM" -eq 1 ] && [ "$OPT_USER" -eq 1 ]; then + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}CONFLICTING ARGUMENTS.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}You cannot specify both${CLEAR_TEXT} ${COMMAND_TEXT}--user${CLEAR_TEXT} ${INFO_TEXT}and${CLEAR_TEXT} ${COMMAND_TEXT}--system${CLEAR_TEXT} ${INFO_TEXT}simultaneously.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + waUsage + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_BAD_ARGUMENT" + fi + + # Simultaneous 'Uninstall' and 'AOSA'. + if [ "$OPT_UNINSTALL" -eq 1 ] && [ "$OPT_AOSA" -eq 1 ]; then + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}CONFLICTING ARGUMENTS.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}You cannot specify both${CLEAR_TEXT} ${COMMAND_TEXT}--uninstall${CLEAR_TEXT} ${INFO_TEXT}and${CLEAR_TEXT} ${COMMAND_TEXT}--aosa${CLEAR_TEXT} ${INFO_TEXT}simultaneously.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + waUsage + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_BAD_ARGUMENT" + fi + + # No 'User' or 'System'. + if [ "$OPT_SYSTEM" -eq 0 ] && [ "$OPT_USER" -eq 0 ]; then + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}INSUFFICIENT ARGUMENTS.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}You must specify either${CLEAR_TEXT} ${COMMAND_TEXT}--user${CLEAR_TEXT} ${INFO_TEXT}or${CLEAR_TEXT} ${COMMAND_TEXT}--system${CLEAR_TEXT} ${INFO_TEXT}to proceed.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + waUsage + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_BAD_ARGUMENT" + fi +} + +# Name: 'waConfigurePathsAndPermissions' +# Role: Sets paths and adjusts permissions as specified. +function waConfigurePathsAndPermissions() { + if [ "$OPT_USER" -eq 1 ]; then + SUDO="" + SOURCE_PATH="$USER_SOURCE_PATH" + BIN_PATH="$USER_BIN_PATH" + APP_PATH="$USER_APP_PATH" + APPDATA_PATH="$USER_APPDATA_PATH" + elif [ "$OPT_SYSTEM" -eq 1 ]; then + SUDO="sudo" + SOURCE_PATH="$SYS_SOURCE_PATH" + BIN_PATH="$SYS_BIN_PATH" + APP_PATH="$SYS_APP_PATH" + APPDATA_PATH="$SYS_APPDATA_PATH" + + # Preemptively obtain superuser privileges. + sudo -v || { + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}AUTHENTICATION FAILURE.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}Failed to gain superuser privileges.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Please check your password and try again." + echo "If you continue to experience issues, contact your system administrator." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_NO_SUDO" + } + fi +} + +# Name: 'waCheckExistingInstall' +# Role: Identifies any existing WinApps installations that may conflict with the new installation. +function waCheckExistingInstall() { + # Print feedback. + echo -n "Checking for existing conflicting WinApps installations... " + + # Check for an existing 'user' installation. + if [[ -f "${USER_BIN_PATH}/winapps" || -d "${USER_SOURCE_PATH}/winapps" ]]; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}EXISTING 'USER' WINAPPS INSTALLATION.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}A previous WinApps installation was detected for the current user.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo -e "Please remove the existing WinApps installation using ${COMMAND_TEXT}winapps-setup --user --uninstall${CLEAR_TEXT}." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_EXISTING_INSTALL" + fi + + # Check for an existing 'system' installation. + if [[ -f "${SYS_BIN_PATH}/winapps" || -d "${SYS_SOURCE_PATH}/winapps" ]]; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}EXISTING 'SYSTEM' WINAPPS INSTALLATION.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}A previous system-wide WinApps installation was detected.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo -e "Please remove the existing WinApps installation using ${COMMAND_TEXT}winapps-setup --system --uninstall${CLEAR_TEXT}." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_EXISTING_INSTALL" + fi + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" +} + +# Name: 'waFixScale' +# Role: Since FreeRDP only supports '/scale' values of 100, 140 or 180, find the closest supported argument to the user's configuration. +function waFixScale() { + # Define variables. + local OLD_SCALE=100 + local VALID_SCALE_1=100 + local VALID_SCALE_2=140 + local VALID_SCALE_3=180 + + # Check for an unsupported value. + if [ "$RDP_SCALE" != "$VALID_SCALE_1" ] && [ "$RDP_SCALE" != "$VALID_SCALE_2" ] && [ "$RDP_SCALE" != "$VALID_SCALE_3" ]; then + # Save the unsupported scale. + OLD_SCALE="$RDP_SCALE" + + # Calculate the absolute differences. + local DIFF_1=$(( RDP_SCALE > VALID_SCALE_1 ? RDP_SCALE - VALID_SCALE_1 : VALID_SCALE_1 - RDP_SCALE )) + local DIFF_2=$(( RDP_SCALE > VALID_SCALE_2 ? RDP_SCALE - VALID_SCALE_2 : VALID_SCALE_2 - RDP_SCALE )) + local DIFF_3=$(( RDP_SCALE > VALID_SCALE_3 ? RDP_SCALE - VALID_SCALE_3 : VALID_SCALE_3 - RDP_SCALE )) + + # Set the final scale to the valid scale value with the smallest absolute difference. + if (( DIFF_1 <= DIFF_2 && DIFF_1 <= DIFF_3 )); then + RDP_SCALE="$VALID_SCALE_1" + elif (( DIFF_2 <= DIFF_1 && DIFF_2 <= DIFF_3 )); then + RDP_SCALE="$VALID_SCALE_2" + else + RDP_SCALE="$VALID_SCALE_3" + fi + + # Print feedback. + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} Unsupported RDP_SCALE value '${OLD_SCALE}' detected. Defaulting to '${RDP_SCALE}'." + fi +} + +# Name: 'waLoadConfig' +# Role: Loads settings specified within the WinApps configuration file. +function waLoadConfig() { + # Print feedback. + echo -n "Attempting to load WinApps configuration file... " + + if [ ! -f "$CONFIG_PATH" ]; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}MISSING CONFIGURATION FILE.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}A valid WinApps configuration file was not found.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo -e "Please create a configuration file at ${COMMAND_TEXT}${CONFIG_PATH}${CLEAR_TEXT}." + echo -e "See https://github.com/winapps-org/winapps?tab=readme-ov-file#step-3-create-a-winapps-configuration-file" + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_NO_CONFIG" + else + # Load the WinApps configuration file. + # shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck. + source "$CONFIG_PATH" + fi + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" +} + +# Name: 'waCheckScriptDependencies' +# Role: Terminate script if dependencies are missing. +function waCheckScriptDependencies() { + # 'Git' + if ! command -v git &>/dev/null; then + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}MISSING DEPENDENCIES.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}Please install 'git' to proceed.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Debian/Ubuntu-based systems:" + echo -e " ${COMMAND_TEXT}sudo apt install git${CLEAR_TEXT}" + echo "Red Hat/Fedora-based systems:" + echo -e " ${COMMAND_TEXT}sudo dnf install git${CLEAR_TEXT}" + echo "Arch Linux systems:" + echo -e " ${COMMAND_TEXT}sudo pacman -S git${CLEAR_TEXT}" + echo "Gentoo Linux systems:" + echo -e " ${COMMAND_TEXT}sudo emerge --ask dev-vcs/git${CLEAR_TEXT}" + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_MISSING_DEPS" + fi + + # 'curl' + if ! command -v curl &>/dev/null; then + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}MISSING DEPENDENCIES.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}Please install 'curl' to proceed.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Debian/Ubuntu-based systems:" + echo -e " ${COMMAND_TEXT}sudo apt install curl${CLEAR_TEXT}" + echo "Red Hat/Fedora-based systems:" + echo -e " ${COMMAND_TEXT}sudo dnf install curl${CLEAR_TEXT}" + echo "Arch Linux systems:" + echo -e " ${COMMAND_TEXT}sudo pacman -S curl${CLEAR_TEXT}" + echo "Gentoo Linux systems:" + echo -e " ${COMMAND_TEXT}sudo emerge --ask net-misc/curl${CLEAR_TEXT}" + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_MISSING_DEPS" + fi + + # 'Dialog'. + if ! command -v dialog &>/dev/null; then + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}MISSING DEPENDENCIES.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}Please install 'dialog' to proceed.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Debian/Ubuntu-based systems:" + echo -e " ${COMMAND_TEXT}sudo apt install dialog${CLEAR_TEXT}" + echo "Red Hat/Fedora-based systems:" + echo -e " ${COMMAND_TEXT}sudo dnf install dialog${CLEAR_TEXT}" + echo "Arch Linux systems:" + echo -e " ${COMMAND_TEXT}sudo pacman -S dialog${CLEAR_TEXT}" + echo "Gentoo Linux systems:" + echo -e " ${COMMAND_TEXT}sudo emerge --ask dialog${CLEAR_TEXT}" + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_MISSING_DEPS" + fi +} + +# Name: 'waCheckInstallDependencies' +# Role: Terminate script if dependencies required to install WinApps are missing. +function waCheckInstallDependencies() { + # Declare variables. + local FREERDP_MAJOR_VERSION="" # Stores the major version of the installed copy of FreeRDP. + + # Print feedback. + echo -n "Checking whether dependencies are installed... " + + # 'libnotify' + if ! command -v notify-send &>/dev/null; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}MISSING DEPENDENCIES.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}Please install 'libnotify' to proceed.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Debian/Ubuntu-based systems:" + echo -e " ${COMMAND_TEXT}sudo apt install libnotify-bin${CLEAR_TEXT}" + echo "Red Hat/Fedora-based systems:" + echo -e " ${COMMAND_TEXT}sudo dnf install libnotify${CLEAR_TEXT}" + echo "Arch Linux systems:" + echo -e " ${COMMAND_TEXT}sudo pacman -S libnotify${CLEAR_TEXT}" + echo "Gentoo Linux systems:" + echo -e " ${COMMAND_TEXT}sudo emerge --ask x11-libs/libnotify${CLEAR_TEXT}" + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_MISSING_DEPS" + fi + + # 'Netcat' + if ! command -v nc &>/dev/null; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}MISSING DEPENDENCIES.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}Please install 'netcat' to proceed.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Debian/Ubuntu-based systems:" + echo -e " ${COMMAND_TEXT}sudo apt install netcat${CLEAR_TEXT}" + echo "Red Hat/Fedora-based systems:" + echo -e " ${COMMAND_TEXT}sudo dnf install nmap-ncat${CLEAR_TEXT}" + echo "Arch Linux systems:" + echo -e " ${COMMAND_TEXT}sudo pacman -S gnu-netcat${CLEAR_TEXT}" + echo "Gentoo Linux systems:" + echo -e " ${COMMAND_TEXT}sudo emerge --ask net-analyzer/netcat${CLEAR_TEXT}" + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_MISSING_DEPS" + fi + + # 'FreeRDP' (Version 3). + # Attempt to set a FreeRDP command if the command variable is empty. + if [ -z "$FREERDP_COMMAND" ]; then + # Check common commands used to launch FreeRDP. + if command -v xfreerdp &>/dev/null; then + # Check FreeRDP major version is 3 or greater. + FREERDP_MAJOR_VERSION=$(xfreerdp --version | head -n 1 | grep -o -m 1 '\b[0-9]\S*' | head -n 1 | cut -d'.' -f1) + if [[ $FREERDP_MAJOR_VERSION =~ ^[0-9]+$ ]] && ((FREERDP_MAJOR_VERSION >= 3)); then + FREERDP_COMMAND="xfreerdp" + fi + fi + + # Check for xfreerdp3 command as a fallback option. + if [ -z "$FREERDP_COMMAND" ]; then + if command -v xfreerdp3 &>/dev/null; then + # Check FreeRDP major version is 3 or greater. + FREERDP_MAJOR_VERSION=$(xfreerdp3 --version | head -n 1 | grep -o -m 1 '\b[0-9]\S*' | head -n 1 | cut -d'.' -f1) + if [[ $FREERDP_MAJOR_VERSION =~ ^[0-9]+$ ]] && ((FREERDP_MAJOR_VERSION >= 3)); then + FREERDP_COMMAND="xfreerdp3" + fi + fi + fi + + # Check for FreeRDP flatpak as a fallback option. + if [ -z "$FREERDP_COMMAND" ]; then + if command -v flatpak &>/dev/null; then + if flatpak list --columns=application | grep -q "^com.freerdp.FreeRDP$"; then + # Check FreeRDP major version is 3 or greater. + FREERDP_MAJOR_VERSION=$(flatpak list --columns=application,version | grep "^com.freerdp.FreeRDP" | awk '{print $2}' | cut -d'.' -f1) + if [[ $FREERDP_MAJOR_VERSION =~ ^[0-9]+$ ]] && ((FREERDP_MAJOR_VERSION >= 3)); then + FREERDP_COMMAND="flatpak run --command=xfreerdp com.freerdp.FreeRDP" + fi + fi + fi + fi + fi + + if ! command -v "$FREERDP_COMMAND" &>/dev/null && [ "$FREERDP_COMMAND" != "flatpak run --command=xfreerdp com.freerdp.FreeRDP" ]; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}MISSING DEPENDENCIES.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}Please install 'FreeRDP' version 3 to proceed.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Debian/Ubuntu-based systems:" + echo -e " ${COMMAND_TEXT}sudo apt install freerdp3-x11${CLEAR_TEXT}" + echo "Red Hat/Fedora-based systems:" + echo -e " ${COMMAND_TEXT}sudo dnf install freerdp${CLEAR_TEXT}" + echo "Arch Linux systems:" + echo -e " ${COMMAND_TEXT}sudo pacman -S freerdp${CLEAR_TEXT}" + echo "Gentoo Linux systems:" + echo -e " ${COMMAND_TEXT}sudo emerge --ask net-misc/freerdp${CLEAR_TEXT}" + echo "" + echo "You can also install FreeRDP as a Flatpak." + echo "Install Flatpak, add the Flathub repository and then install FreeRDP:" + echo -e "${COMMAND_TEXT}flatpak install flathub com.freerdp.FreeRDP${CLEAR_TEXT}" + echo -e "${COMMAND_TEXT}sudo flatpak override --filesystem=home com.freerdp.FreeRDP${CLEAR_TEXT}" + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_MISSING_DEPS" + fi + + # 'libvirt'/'virt-manager' + 'iproute2'. + if [ "$WAFLAVOR" = "libvirt" ]; then + if ! command -v virsh &>/dev/null; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}MISSING DEPENDENCIES.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}Please install 'Virtual Machine Manager' to proceed.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Debian/Ubuntu-based systems:" + echo -e " ${COMMAND_TEXT}sudo apt install virt-manager${CLEAR_TEXT}" + echo "Red Hat/Fedora-based systems:" + echo -e " ${COMMAND_TEXT}sudo dnf install virt-manager${CLEAR_TEXT}" + echo "Arch Linux systems:" + echo -e " ${COMMAND_TEXT}sudo pacman -S virt-manager${CLEAR_TEXT}" + echo "Gentoo Linux systems:" + echo -e " ${COMMAND_TEXT}sudo emerge --ask app-emulation/virt-manager${CLEAR_TEXT}" + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_MISSING_DEPS" + fi + + if ! command -v ip &>/dev/null; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}MISSING DEPENDENCIES.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}Please install 'iproute2' to proceed.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Debian/Ubuntu-based systems:" + echo -e " ${COMMAND_TEXT}sudo apt install iproute2${CLEAR_TEXT}" + echo "Red Hat/Fedora-based systems:" + echo -e " ${COMMAND_TEXT}sudo dnf install iproute${CLEAR_TEXT}" + echo "Arch Linux systems:" + echo -e " ${COMMAND_TEXT}sudo pacman -S iproute2${CLEAR_TEXT}" + echo "Gentoo Linux systems:" + echo -e " ${COMMAND_TEXT}sudo emerge --ask net-misc/iproute2${CLEAR_TEXT}" + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_MISSING_DEPS" + fi + elif [ "$WAFLAVOR" = "docker" ]; then + if ! command -v docker &>/dev/null; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}MISSING DEPENDENCIES.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}Please install 'Docker Engine' to proceed.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Please visit https://docs.docker.com/engine/install/ for more information." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_MISSING_DEPS" + fi + elif [ "$WAFLAVOR" = "podman" ]; then + if ! command -v podman-compose &>/dev/null || ! command -v podman &>/dev/null; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}MISSING DEPENDENCIES.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}Please install 'podman' and 'podman-compose' to proceed.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Please visit https://podman.io/docs/installation for more information." + echo "Please visit https://github.com/containers/podman-compose for more information." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_MISSING_DEPS" + fi + fi + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" +} + +# Name: 'waCheckGroupMembership' +# Role: Ensures the current user is part of the required groups. +function waCheckGroupMembership() { + # Print feedback. + echo -n "Checking whether the user '$(whoami)' is part of the required groups... " + + # Declare variables. + local USER_GROUPS="" # Stores groups the current user belongs to. + + # Identify groups the current user belongs to. + USER_GROUPS=$(groups "$(whoami)") + + if ! (echo "$USER_GROUPS" | grep -q -E "\blibvirt\b") || ! (echo "$USER_GROUPS" | grep -q -E "\bkvm\b"); then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}GROUP MEMBERSHIP CHECK ERROR.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}The current user '$(whoami)' is not part of group 'libvirt' and/or group 'kvm'.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Please run the below commands, followed by a system reboot:" + echo -e "${COMMAND_TEXT}sudo usermod -a -G libvirt $(whoami)${CLEAR_TEXT}" + echo -e "${COMMAND_TEXT}sudo usermod -a -G kvm $(whoami)${CLEAR_TEXT}" + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_NOT_IN_GROUP" + fi + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" +} + +# Name: 'waCheckVMRunning' +# Role: Checks the state of the Windows 'libvirt' VM to ensure it is running. +function waCheckVMRunning() { + # Print feedback. + echo -n "Checking the status of the Windows VM... " + + # Declare variables. + local VM_STATE="" # Stores the state of the Windows VM. + + # Obtain VM Status + VM_PAUSED=0 + virsh list --state-paused | grep -wq "$VM_NAME" || VM_PAUSED="$?" + VM_RUNNING=0 + virsh list --state-running | grep -wq "$VM_NAME" || VM_RUNNING="$?" + VM_SHUTOFF=0 + virsh list --state-shutoff | grep -wq "$VM_NAME" || VM_SHUTOFF="$?" + + if [[ $VM_SHUTOFF == "0" ]]; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}WINDOWS VM NOT RUNNING.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}The Windows VM '${VM_NAME}' is powered off.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Please run the below command to start the Windows VM:" + echo -e "${COMMAND_TEXT}virsh start ${VM_NAME}${CLEAR_TEXT}" + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_VM_OFF" + elif [[ $VM_PAUSED == "0" ]]; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}WINDOWS VM NOT RUNNING.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}The Windows VM '${VM_NAME}' is paused.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Please run the below command to resume the Windows VM:" + echo -e "${COMMAND_TEXT}virsh resume ${VM_NAME}${CLEAR_TEXT}" + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_VM_PAUSED" + elif [[ $VM_RUNNING != "0" ]]; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}WINDOWS VM DOES NOT EXIST.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}The Windows VM '${VM_NAME}' could not be found.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Please ensure a Windows VM with the name '${VM_NAME}' exists." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_VM_ABSENT" + fi + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" +} + +# Name: 'waCheckContainerRunning' +# Role: Throw an error if the Docker/Podman container is not running. +function waCheckContainerRunning() { + # Print feedback. + echo -n "Checking container status... " + + # Declare variables. + local CONTAINER_STATE="" + local COMPOSE_COMMAND="" + + # Determine the state of the container. + CONTAINER_STATE=$("$WAFLAVOR" ps --all --filter name="winapps" --format '{{.Status}}') + CONTAINER_STATE=${CONTAINER_STATE,,} # Convert the string to lowercase. + CONTAINER_STATE=${CONTAINER_STATE%% *} # Extract the first word. + + # Determine the compose command. + case "$WAFLAVOR" in + "docker") COMPOSE_COMMAND="docker compose" ;; + "podman") COMPOSE_COMMAND="podman-compose" ;; + esac + + # Check container state. + if [[ "$CONTAINER_STATE" != "up" ]]; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}CONTAINER NOT RUNNING.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}Windows is not running.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Please ensure Windows is powered on:" + echo -e "${COMMAND_TEXT}${COMPOSE_COMMAND} --file ~/.config/winapps/compose.yaml start${CLEAR_TEXT}" + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_CONTAINER_OFF" + fi + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" +} + +# Name: 'waCheckPortOpen' +# Role: Assesses whether the RDP port on Windows is open. +function waCheckPortOpen() { + # Print feedback. + echo -n "Checking for an open RDP Port on Windows... " + + # Declare variables. + local VM_MAC="" # Stores the MAC address of the Windows VM. + + # Obtain Windows VM IP Address (FOR 'libvirt' ONLY) + # Note: 'RDP_IP' should not be empty if 'WAFLAVOR' is 'docker', since it is set to localhost before this function is called. + if [ -z "$RDP_IP" ] && [ "$WAFLAVOR" = "libvirt" ]; then + VM_MAC=$(virsh domiflist "$VM_NAME" | grep -oE "([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})") # VM MAC address. + RDP_IP=$(ip neigh show | grep "$VM_MAC" | grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}") # VM IP address. + + if [ -z "$RDP_IP" ]; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}NETWORK CONFIGURATION ERROR.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}The IP address of the Windows VM '${VM_NAME}' could not be found.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Please ensure networking is properly configured for the Windows VM." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_NO_IP" + fi + fi + + # Check for an open RDP port. + if ! timeout "$PORT_TIMEOUT" nc -z "$RDP_IP" "$RDP_PORT" &>/dev/null; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}NETWORK CONFIGURATION ERROR.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}Failed to establish a connection with Windows at '${RDP_IP}:${RDP_PORT}'.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Please ensure Remote Desktop is configured on Windows as per the WinApps README." + echo -e "Then you can try increasing the ${COMMAND_TEXT}PORT_TIMEOUT${CLEAR_TEXT} in ${COMMAND_TEXT}${CONFIG_PATH}${CLEAR_TEXT}." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_BAD_PORT" + fi + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" +} + +# Name: 'waCheckRDPAccess' +# Role: Tests if Windows is accessible via RDP. +function waCheckRDPAccess() { + # Print feedback. + echo -n "Attempting to establish a Remote Desktop connection with Windows... " + + # Declare variables. + local FREERDP_LOG="" # Stores the path of the FreeRDP log file. + local FREERDP_PROC="" # Stores the FreeRDP process ID. + local ELAPSED_TIME="" # Stores the time counter. + + # Log file path. + FREERDP_LOG="${USER_APPDATA_PATH}/FreeRDP_Test_$(date +'%Y%m%d_%H%M_%N').log" + + # Ensure the output directory exists. + mkdir -p "$USER_APPDATA_PATH" + + # Remove existing 'FreeRDP Connection Test' file. + rm -f "$TEST_PATH" + + # This command should create a file on the host filesystem before terminating the RDP session. This command is silently executed as a background process. + # If the file is created, it means Windows received the command via FreeRDP successfully and can read and write to the Linux home folder. + # Note: The following final line is expected within the log, indicating successful execution of the 'tsdiscon' command and termination of the RDP session. + # [INFO][com.freerdp.core] - [rdp_print_errinfo]: ERRINFO_LOGOFF_BY_USER (0x0000000C):The disconnection was initiated by the user logging off their session on the server. + # shellcheck disable=SC2140,SC2027 # Disable warnings regarding unquoted strings. + $FREERDP_COMMAND \ + /cert:tofu \ + /d:"$RDP_DOMAIN" \ + /u:"$RDP_USER" \ + /p:"$RDP_PASS" \ + /scale:"$RDP_SCALE" \ + +auto-reconnect \ + /app:program:"C:\Windows\System32\cmd.exe",cmd:"/C type NUL > $TEST_PATH_WIN && tsdiscon" \ + /v:"$RDP_IP" &>"$FREERDP_LOG" & + + # Store the FreeRDP process ID. + FREERDP_PROC=$! + + # Initialise the time counter. + ELAPSED_TIME=0 + + # Wait a maximum of $RDP_TIMEOUT seconds for the background process to complete. + while [ "$ELAPSED_TIME" -lt "$RDP_TIMEOUT" ]; do + # Check if the FreeRDP process is complete or if the test file exists. + if ! ps -p "$FREERDP_PROC" &>/dev/null || [ -f "$TEST_PATH" ]; then + break + fi + + # Wait for 5 seconds. + sleep 5 + ELAPSED_TIME=$((ELAPSED_TIME + 5)) + done + + # Check if FreeRDP process is not complete. + if ps -p "$FREERDP_PROC" &>/dev/null; then + # SIGKILL FreeRDP. + kill -9 "$FREERDP_PROC" &>/dev/null + fi + + # Check if test file does not exist. + if ! [ -f "$TEST_PATH" ]; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}REMOTE DESKTOP PROTOCOL FAILURE.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}FreeRDP failed to establish a connection with Windows.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo -e "Please view the log at ${COMMAND_TEXT}${FREERDP_LOG}${CLEAR_TEXT}." + echo "Troubleshooting Tips:" + echo " - Ensure the user is logged out of Windows prior to initiating the WinApps installation." + echo " - Ensure the credentials within the WinApps configuration file are correct." + echo -e " - Utilise a new certificate by removing relevant certificate(s) in ${COMMAND_TEXT}${HOME}/.config/freerdp/server${CLEAR_TEXT}." + echo -e " - Try increasing the ${COMMAND_TEXT}RDP_TIMEOUT${CLEAR_TEXT} in ${COMMAND_TEXT}${CONFIG_PATH}${CLEAR_TEXT}." + echo " - If using 'libvirt', ensure the Windows VM is correctly named as specified within the README." + echo " - If using 'libvirt', ensure 'Remote Desktop' is enabled within the Windows VM." + echo " - If using 'libvirt', ensure you have merged 'RDPApps.reg' into the Windows VM's registry." + echo " - If using 'libvirt', try logging into and back out of the Windows VM within 'virt-manager' prior to initiating the WinApps installation." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_RDP_FAIL" + else + # Remove the temporary test file. + rm -f "$TEST_PATH" + fi + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" +} + +# Name: 'waFindInstalled' +# Role: Identifies installed applications on Windows. +function waFindInstalled() { + # Print feedback. + echo -n "Checking for installed Windows applications... " + + # Declare variables. + local FREERDP_LOG="" # Stores the path of the FreeRDP log file. + local FREERDP_PROC="" # Stores the FreeRDP process ID. + local ELAPSED_TIME="" # Stores the time counter. + + # Log file path. + FREERDP_LOG="${USER_APPDATA_PATH}/FreeRDP_Scan_$(date +'%Y%m%d_%H%M_%N').log" + + # Make the output directory if required. + mkdir -p "$USER_APPDATA_PATH" + + # Remove temporary files from previous WinApps installations. + rm -f "$BATCH_SCRIPT_PATH" "$TMP_INST_FILE_PATH" "$INST_FILE_PATH" "$PS_SCRIPT_HOME_PATH" "$DETECTED_FILE_PATH" + + # Copy PowerShell script to a directory within the user's home folder. + # This will enable the PowerShell script to be accessed and executed by Windows. + cp "$PS_SCRIPT_PATH" "$PS_SCRIPT_HOME_PATH" + + # Enumerate over each officially supported application. + for APPLICATION in ./apps/*; do + # Extract the name of the application from the absolute path of the folder. + APPLICATION="$(basename "$APPLICATION")" + + if [[ "$APPLICATION" == "ms-office-protocol-handler.desktop" ]]; then + continue + fi + + # Source 'Info' File Containing: + # - The Application Name (FULL_NAME) + # - The Shortcut Name (NAME) + # - Application Categories (CATEGORIES) + # - Executable Path (WIN_EXECUTABLE) + # - Supported MIME Types (MIME_TYPES) + # - Application Icon (ICON) + # shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck. + source "./apps/${APPLICATION}/info" + + # Append commands to batch file. + echo "IF EXIST \"${WIN_EXECUTABLE}\" ECHO ${APPLICATION} >> ${TMP_INST_FILE_PATH_WIN}" >>"$BATCH_SCRIPT_PATH" + done + + # Append a command to the batch script to run the PowerShell script and store its output in the 'detected' file. + # shellcheck disable=SC2129 # Silence warning regarding repeated redirects. + echo "powershell.exe -ExecutionPolicy Bypass -File ${PS_SCRIPT_HOME_PATH_WIN} > ${DETECTED_FILE_PATH_WIN}" >>"$BATCH_SCRIPT_PATH" + + # Append a command to the batch script to rename the temporary file containing the names of all detected officially supported applications. + echo "RENAME ${TMP_INST_FILE_PATH_WIN} installed" >>"$BATCH_SCRIPT_PATH" + + # Append a command to the batch script to terminate the remote desktop session once all previous commands are complete. + echo "tsdiscon" >>"$BATCH_SCRIPT_PATH" + + # Silently execute the batch script within Windows in the background (Log Output To File) + # Note: The following final line is expected within the log, indicating successful execution of the 'tsdiscon' command and termination of the RDP session. + # [INFO][com.freerdp.core] - [rdp_print_errinfo]: ERRINFO_LOGOFF_BY_USER (0x0000000C):The disconnection was initiated by the user logging off their session on the server. + # shellcheck disable=SC2140,SC2027 # Disable warnings regarding unquoted strings. + $FREERDP_COMMAND \ + /cert:tofu \ + /d:"$RDP_DOMAIN" \ + /u:"$RDP_USER" \ + /p:"$RDP_PASS" \ + /scale:"$RDP_SCALE" \ + +auto-reconnect \ + /app:program:"C:\Windows\System32\cmd.exe",cmd:"/C "$BATCH_SCRIPT_PATH_WIN"" \ + /v:"$RDP_IP" &>"$FREERDP_LOG" & + + # Store the FreeRDP process ID. + FREERDP_PROC=$! + + # Initialise the time counter. + ELAPSED_TIME=0 + + # Wait a maximum of $APP_SCAN_TIMEOUT seconds for the batch script to finish running. + while [ $ELAPSED_TIME -lt "$APP_SCAN_TIMEOUT" ]; do + # Check if the FreeRDP process is complete or if the 'installed' file exists. + if ! ps -p "$FREERDP_PROC" &>/dev/null || [ -f "$INST_FILE_PATH" ]; then + break + fi + + # Wait for 5 seconds. + sleep 5 + ELAPSED_TIME=$((ELAPSED_TIME + 5)) + done + + # Check if the FreeRDP process is not complete. + if ps -p "$FREERDP_PROC" &>/dev/null; then + # SIGKILL FreeRDP. + kill -9 "$FREERDP_PROC" &>/dev/null + fi + + # Check if test file does not exist. + if ! [ -f "$INST_FILE_PATH" ]; then + # Complete the previous line. + echo -e "${FAIL_TEXT}Failed!${CLEAR_TEXT}\n" + + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}APPLICATION QUERY FAILURE.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}Failed to query Windows for installed applications.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo -e "Please view the log at ${COMMAND_TEXT}${FREERDP_LOG}${CLEAR_TEXT}." + echo -e "You can try increasing the ${COMMAND_TEXT}APP_SCAN_TIMEOUT${CLEAR_TEXT} in ${COMMAND_TEXT}${CONFIG_PATH}${CLEAR_TEXT}." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_APPQUERY_FAIL" + fi + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" +} + +# Name: 'waConfigureWindows' +# Role: Create an application entry for launching Windows via Remote Desktop. +function waConfigureWindows() { + # Print feedback. + echo -n "Creating an application entry for Windows... " + + # Declare variables. + local WIN_BASH="" # Stores the bash script to launch a Windows RDP session. + local WIN_DESKTOP="" # Stores the '.desktop' file to launch a Windows RDP session. + + # Populate variables. + WIN_BASH="\ +#!/usr/bin/env bash +${BIN_PATH}/winapps windows" + WIN_DESKTOP="\ +[Desktop Entry] +Name=Windows +Exec=${BIN_PATH}/winapps windows %F +Terminal=false +Type=Application +Icon=${APPDATA_PATH}/icons/windows.svg +StartupWMClass=Microsoft Windows +Comment=Microsoft Windows RDP Session" + + # Copy the 'Windows' icon. + $SUDO cp "./icons/windows.svg" "${APPDATA_PATH}/icons/windows.svg" + + # Write the desktop entry content to a file. + echo "$WIN_DESKTOP" | $SUDO tee "${APP_PATH}/windows.desktop" &>/dev/null + + # Write the bash script to a file. + echo "$WIN_BASH" | $SUDO tee "${BIN_PATH}/windows" &>/dev/null + + # Mark the bash script as executable. + $SUDO chmod a+x "${BIN_PATH}/windows" + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" +} + +# Name: 'waConfigureApp' +# Role: Create application entries for a given application installed on Windows. +function waConfigureApp() { + # Declare variables. + local APP_ICON="" # Stores the path to the application icon. + local APP_BASH="" # Stores the bash script used to launch the application. + local APP_DESKTOP_FILE="" # Stores the '.desktop' file used to launch the application. + + # Source 'Info' File Containing: + # - The Application Name (FULL_NAME) + # - The Shortcut Name (NAME) + # - Application Categories (CATEGORIES) + # - Executable Path (WIN_EXECUTABLE) + # - Supported MIME Types (MIME_TYPES) + # - Application Icon (ICON) + # shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck. + source "${APPDATA_PATH}/apps/${1}/info" + + # Determine path to application icon using arguments passed to function. + APP_ICON="${APPDATA_PATH}/apps/${1}/icon.${2}" + + # Determine the content of the bash script for the application. + APP_BASH="\ +#!/usr/bin/env bash +${BIN_PATH}/winapps ${1}" + + # Determine the content of the '.desktop' file for the application. + APP_DESKTOP_FILE="\ +[Desktop Entry] +Name=${NAME} +Exec=${BIN_PATH}/winapps ${1} %F +Terminal=false +Type=Application +Icon=${APP_ICON} +StartupWMClass=${FULL_NAME} +Comment=${FULL_NAME} +Categories=${CATEGORIES} +MimeType=${MIME_TYPES}" + + # Store the '.desktop' file for the application. + echo "$APP_DESKTOP_FILE" | $SUDO tee "${APP_PATH}/${1}.desktop" &>/dev/null + + # Store the bash script for the application. + echo "$APP_BASH" | $SUDO tee "${BIN_PATH}/${1}" &>/dev/null + + # Mark bash script as executable. + $SUDO chmod a+x "${BIN_PATH}/${1}" +} + +# Name: 'waConfigureOfficiallySupported' +# Role: Create application entries for officially supported applications installed on Windows. +function waConfigureOfficiallySupported() { + # Declare variables. + local OSA_LIST=() # Stores a list of all officially supported applications installed on Windows. + local OFFICE_APPS=("access" "access-o365" "access-o365-x86" "access-x86" "adobe-cc" "acrobat9" "acrobat-x-pro" "aftereffects-cc" "audition-cc" "bridge-cc" "bridge-cc-x86" "bridge-cs6" "bridge-cs6-x86" "cmd" "dymo-connect" "excel" "excel-o365" "excel-o365-x86" "excel-x86" "excel-x86-2010" "explorer" "iexplorer" "illustrator-cc" "lightroom-cc" "linqpad8" "mirc" "mspaint" "onenote" "onenote-o365" "onenote-o365-x86" "onenote-x86" "outlook" "outlook-o365" "outlook-o365-x86" "powerpoint" "powerpoint-o365" "powerpoint-o365-x86" "powerpoint-x86" "publisher" "publisher-o365" "publisher-o365-x86" "publisher-x86" "project" "project-x86" "remarkable-desktop" "ssms20" "visual-studio-comm" "visual-studio-ent" "visual-studio-pro" "visio" "visio-x86" "word" "word-o365" "word-o365-x86" "word-x86" "word-x86-2010") + + # Read the list of officially supported applications that are installed on Windows into an array, returning an empty array if no such files exist. + readarray -t OSA_LIST < <(grep -v '^[[:space:]]*$' "$INST_FILE_PATH" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' 2>/dev/null || true) + + # Create application entries for each officially supported application. + for OSA in "${OSA_LIST[@]}"; do + # Print feedback. + echo -n "Creating an application entry for ${OSA}... " + + # Copy application icon and information. + $SUDO cp -r "./apps/${OSA}" "${APPDATA_PATH}/apps" + + # Configure the application. + waConfigureApp "$OSA" svg + + # Check if the application is an Office app and copy the protocol handler. + if [[ " ${OFFICE_APPS[*]} " == *" $OSA "* ]]; then + # Determine the target directory based on whether the installation is for the system or user. + if [[ "$OPT_SYSTEM" -eq 1 ]]; then + TARGET_DIR="$SYS_APP_PATH" + else + TARGET_DIR="$USER_APP_PATH" + fi + + # Copy the protocol handler to the appropriate directory. + $SUDO cp "./apps/ms-office-protocol-handler.desktop" "$TARGET_DIR/ms-office-protocol-handler.desktop" + fi + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" + done + + # Delete 'install' file. + rm -f "$INST_FILE_PATH" +} + +# Name: 'waConfigureApps' +# Role: Allow the user to select which officially supported applications to configure. +function waConfigureApps() { + # Declare variables. + local OSA_LIST=() # Stores a list of all officially supported applications installed on Windows. + local APPS=() # Stores a list of both the simplified and full names of each installed officially supported application. + local OPTIONS=() # Stores a list of options presented to the user. + local APP_INSTALL="" # Stores the option selected by the user. + local SELECTED_APPS=() # Stores the officially supported applications selected by the user. + local TEMP_ARRAY=() # Temporary array used for sorting elements of an array. + + # Read the list of officially supported applications that are installed on Windows into an array, returning an empty array if no such files exist. + # This will remove leading and trailing whitespace characters as well as ignore empty lines. + readarray -t OSA_LIST < <(grep -v '^[[:space:]]*$' "$INST_FILE_PATH" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' 2>/dev/null || true) + + # Loop over each officially supported application installed on Windows. + for OSA in "${OSA_LIST[@]}"; do + # Source 'Info' File Containing: + # - The Application Name (FULL_NAME) + # - The Shortcut Name (NAME) + # - Application Categories (CATEGORIES) + # - Executable Path (WIN_EXECUTABLE) + # - Supported MIME Types (MIME_TYPES) + # - Application Icon (ICON) + # shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck. + source "./apps/${OSA}/info" + + # Add both the simplified and full name of the application to an array. + APPS+=("${FULL_NAME} (${OSA})") + + # Extract the executable file name (e.g. 'MyApp.exe') from the absolute path. + WIN_EXECUTABLE="${WIN_EXECUTABLE##*\\}" + + # Trim any leading or trailing whitespace characters from the executable file name. + read -r WIN_EXECUTABLE <<<"$(echo "$WIN_EXECUTABLE" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + + # Add the executable file name (in lowercase) to the array. + INSTALLED_EXES+=("${WIN_EXECUTABLE,,}") + done + + # Sort the 'APPS' array in alphabetical order. + IFS=$'\n' + # shellcheck disable=SC2207 # Silence warnings regarding preferred use of 'mapfile' or 'read -a'. + TEMP_ARRAY=($(sort <<<"${APPS[*]}")) + unset IFS + APPS=("${TEMP_ARRAY[@]}") + + # Prompt user to select which officially supported applications to configure. + OPTIONS=( + "Set up all detected officially supported applications" + "Choose specific officially supported applications to set up" + "Skip setting up any officially supported applications" + ) + inqMenu "How would you like to handle officially supported applications?" OPTIONS APP_INSTALL + + # Remove unselected officially supported applications from the 'install' file. + if [[ $APP_INSTALL == "Choose specific officially supported applications to set up" ]]; then + inqChkBx "Which officially supported applications would you like to set up?" APPS SELECTED_APPS + + # Clear/create the 'install' file. + echo "" >"$INST_FILE_PATH" + + # Add each selected officially supported application back to the 'install' file. + for SELECTED_APP in "${SELECTED_APPS[@]}"; do + # Capture the substring within (but not including) the parentheses. + # This substring represents the officially supported application name (see above loop). + SELECTED_APP="${SELECTED_APP##*(}" + SELECTED_APP="${SELECTED_APP%%)}" + + # Add the substring back to the 'install' file. + echo "$SELECTED_APP" >>"$INST_FILE_PATH" + done + fi + + # Configure selected (or all) officially supported applications. + if [[ $APP_INSTALL != "Skip setting up any officially supported applications" ]]; then + waConfigureOfficiallySupported + fi +} + +# Name: 'waConfigureDetectedApps' +# Role: Allow the user to select which detected applications to configure. +function waConfigureDetectedApps() { + # Declare variables. + local APPS=() # Stores a list of both the simplified and full names of each detected application. + local EXE_FILENAME="" # Stores the executable filename of a given detected application. + local EXE_FILENAME_NOEXT="" # Stores the executable filename without the file extension of a given detected application. + local EXE_FILENAME_LOWERCASE="" # Stores the executable filename of a given detected application in lowercase letters only. + local OPTIONS=() # Stores a list of options presented to the user. + local APP_INSTALL="" # Stores the option selected by the user. + local SELECTED_APPS=() # Detected applications selected by the user. + local APP_DESKTOP_FILE="" # Stores the '.desktop' file used to launch the application. + local TEMP_ARRAY=() # Temporary array used for sorting elements of an array. + + if [ -f "$DETECTED_FILE_PATH" ]; then + # On UNIX systems, lines are terminated with a newline character (\n). + # On WINDOWS systems, lines are terminated with both a carriage return (\r) and a newline (\n) character. + # Remove all carriage returns (\r) within the 'detected' file, as the file was written by Windows. + sed -i 's/\r//g' "$DETECTED_FILE_PATH" + + # Import the detected application information: + # - Application Names (NAMES) + # - Application Icons in base64 (ICONS) + # - Application Executable Paths (EXES) + # shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck. + source "$DETECTED_FILE_PATH" + + # shellcheck disable=SC2153 # Silence warnings regarding possible misspellings. + for INDEX in "${!NAMES[@]}"; do + # Extract the executable file name (e.g. 'MyApp.exe'). + EXE_FILENAME=${EXES[$INDEX]##*\\} + + # Convert the executable file name to lower-case (e.g. 'myapp.exe'). + EXE_FILENAME_LOWERCASE="${EXE_FILENAME,,}" + + # Remove the file extension (e.g. 'MyApp'). + EXE_FILENAME_NOEXT="${EXE_FILENAME%.*}" + + # Check if the executable was previously configured as part of setting up officially supported applications. + if [[ " ${INSTALLED_EXES[*]} " != *" ${EXE_FILENAME_LOWERCASE} "* ]]; then + # If not previously configured, add the application to the list of detected applications. + APPS+=("${NAMES[$INDEX]} (${EXE_FILENAME_NOEXT})") + fi + done + + # Sort the 'APPS' array in alphabetical order. + IFS=$'\n' + # shellcheck disable=SC2207 # Silence warnings regarding preferred use of 'mapfile' or 'read -a'. + TEMP_ARRAY=($(sort <<<"${APPS[*]}")) + unset IFS + APPS=("${TEMP_ARRAY[@]}") + + # Prompt user to select which other detected applications to configure. + OPTIONS=( + "Set up all detected applications" + "Select which applications to set up" + "Do not set up any applications" + ) + inqMenu "How would you like to handle other detected applications?" OPTIONS APP_INSTALL + + # Store selected detected applications. + if [[ $APP_INSTALL == "Select which applications to set up" ]]; then + inqChkBx "Which other applications would you like to set up?" APPS SELECTED_APPS + elif [[ $APP_INSTALL == "Set up all detected applications" ]]; then + for APP in "${APPS[@]}"; do + SELECTED_APPS+=("$APP") + done + fi + + for SELECTED_APP in "${SELECTED_APPS[@]}"; do + # Capture the substring within (but not including) the parentheses. + # This substring represents the executable filename without the file extension (see above loop). + EXE_FILENAME_NOEXT="${SELECTED_APP##*(}" + EXE_FILENAME_NOEXT="${EXE_FILENAME_NOEXT%%)}" + + # Capture the substring prior to the space and parentheses. + # This substring represents the detected application name (see above loop). + PROGRAM_NAME="${SELECTED_APP% (*}" + + # Loop through all detected applications to find the detected application being processed. + for INDEX in "${!NAMES[@]}"; do + # Check for a matching detected application entry. + if [[ ${NAMES[$INDEX]} == "$PROGRAM_NAME" ]] && [[ ${EXES[$INDEX]} == *"\\$EXE_FILENAME_NOEXT"* ]]; then + # Print feedback. + echo -n "Creating an application entry for ${PROGRAM_NAME}... " + + # Create directory to store application icon and information. + $SUDO mkdir -p "${APPDATA_PATH}/apps/${EXE_FILENAME_NOEXT}" + + # Determine the content of the '.desktop' file for the application. + APP_DESKTOP_FILE="\ +# GNOME Shortcut Name +NAME=\"${PROGRAM_NAME}\" +# Used for Descriptions and Window Class +FULL_NAME=\"${PROGRAM_NAME}\" +# Path to executable inside Windows +WIN_EXECUTABLE=\"${EXES[$INDEX]}\" +# GNOME Categories +CATEGORIES=\"WinApps\" +# GNOME MIME Types +MIME_TYPES=\"\"" + + # Store the '.desktop' file for the application. + echo "$APP_DESKTOP_FILE" | $SUDO tee "${APPDATA_PATH}/apps/${EXE_FILENAME_NOEXT}/info" &>/dev/null + + # Write application icon to file. + echo "${ICONS[$INDEX]}" | base64 -d | $SUDO tee "${APPDATA_PATH}/apps/${EXE_FILENAME_NOEXT}/icon.png" &>/dev/null + + # Configure the application. + waConfigureApp "$EXE_FILENAME_NOEXT" png + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" + fi + done + done + fi +} + +# Name: 'waInstall' +# Role: Installs WinApps. +function waInstall() { + # Print feedback. + echo -e "${BOLD_TEXT}Installing WinApps.${CLEAR_TEXT}" + + # Check for existing conflicting WinApps installations. + waCheckExistingInstall + + # Load the WinApps configuration file. + waLoadConfig + + # Check for missing dependencies. + waCheckInstallDependencies + + # Update $RDP_SCALE. + waFixScale + + # Append additional FreeRDP flags if required. + if [[ -n $RDP_FLAGS ]]; then + FREERDP_COMMAND="${FREERDP_COMMAND} ${RDP_FLAGS}" + fi + + # If using 'docker' or 'podman', set RDP_IP to localhost. + if [ "$WAFLAVOR" = "docker" ] || [ "$WAFLAVOR" = "podman" ]; then + RDP_IP="$DOCKER_IP" + fi + + # If using podman backend, modify the FreeRDP command to enter a new namespace. + if [ "$WAFLAVOR" = "podman" ]; then + FREERDP_COMMAND="podman unshare --rootless-netns ${FREERDP_COMMAND}" + fi + + if [ "$WAFLAVOR" = "docker" ] || [ "$WAFLAVOR" = "podman" ]; then + # Check if Windows is powered on. + waCheckContainerRunning + elif [ "$WAFLAVOR" = "libvirt" ]; then + # Verify the current user's group membership. + waCheckGroupMembership + + # Check if the Windows VM is powered on. + waCheckVMRunning + elif [ "$WAFLAVOR" = "manual" ]; then + waCheckPortOpen + else + # Display the error type. + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}INVALID WINAPPS BACKEND.${CLEAR_TEXT}" + + # Display the error details. + echo -e "${INFO_TEXT}An invalid WinApps backend '${WAFLAVOR}' was specified.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo -e "Please ensure 'WAFLAVOR' is set to 'docker', 'podman' or 'libvirt' in ${COMMAND_TEXT}${CONFIG_PATH}${CLEAR_TEXT}." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_INVALID_FLAVOR" + fi + + # Check if the RDP port on Windows is open. + waCheckPortOpen + + # Test RDP access to Windows. + waCheckRDPAccess + + # Create required directories. + $SUDO mkdir -p "$BIN_PATH" + $SUDO mkdir -p "$APP_PATH" + $SUDO mkdir -p "$APPDATA_PATH/apps" + $SUDO mkdir -p "$APPDATA_PATH/icons" + + # Check for installed applications. + waFindInstalled + + # Install the WinApps bash scripts. + $SUDO ln -sf "${SOURCE_PATH}/bin/winapps" "${BIN_PATH}/winapps" + $SUDO ln -sf "${SOURCE_PATH}/setup.sh" "${BIN_PATH}/winapps-setup" + + # Configure the Windows RDP session application launcher. + waConfigureWindows + + if [ "$OPT_AOSA" -eq 1 ]; then + # Automatically configure all officially supported applications. + waConfigureOfficiallySupported + else + # Configure officially supported applications. + waConfigureApps + + # Configure other detected applications. + waConfigureDetectedApps + fi + + # Ensure BIN_PATH is on PATH + waEnsureOnPath + + # Print feedback. + echo -e "${SUCCESS_TEXT}INSTALLATION COMPLETE.${CLEAR_TEXT}" +} + +# Name: 'waEnsureOnPath' +# Role: Ensures that $BIN_PATH is on $PATH. +function waEnsureOnPath() { + if [[ ":$PATH:" != *":$BIN_PATH:"* ]]; then + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} It seems like '${BIN_PATH}' is not on PATH." + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} You can add it by running:" + # shellcheck disable=SC2086 + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} - For Bash: ${COMMAND_TEXT}echo 'export PATH="${BIN_PATH}:\$PATH"' >> ~/.bashrc && source ~/.bashrc${CLEAR_TEXT}" + # shellcheck disable=SC2086 + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} - For ZSH: ${COMMAND_TEXT}echo 'export PATH="${BIN_PATH}:\$PATH"' >> ~/.zshrc && source ~/.zshrc${CLEAR_TEXT}" + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} Make sure to restart your Terminal afterwards.\n" + fi +} + +# Name: 'waUninstall' +# Role: Uninstalls WinApps. +function waUninstall() { + + # Print feedback. + [ "$OPT_SYSTEM" -eq 1 ] && echo -e "${BOLD_TEXT}REMOVING SYSTEM INSTALLATION.${CLEAR_TEXT}" + [ "$OPT_USER" -eq 1 ] && echo -e "${BOLD_TEXT}REMOVING USER INSTALLATION.${CLEAR_TEXT}" + + # Determine the target directory for the protocol handler based on the installation type. + if [[ "$OPT_SYSTEM" -eq 1 ]]; then + TARGET_DIR="$SYS_APP_PATH" + else + TARGET_DIR="$USER_APP_PATH" + fi + + # Remove the 'ms-office-protocol-handler.desktop' file if it exists. + $SUDO rm -f "$TARGET_DIR/ms-office-protocol-handler.desktop" + + # Declare variables. + local WINAPPS_DESKTOP_FILES=() # Stores a list of '.desktop' file paths. + local WINAPPS_APP_BASH_SCRIPTS=() # Stores a list of bash script paths. + local DESKTOP_FILE_NAME="" # Stores the name of the '.desktop' file for the application. + local BASH_SCRIPT_NAME="" # Stores the name of the application. + + # Remove the 'WinApps' bash scripts. + $SUDO rm -f "${BIN_PATH}/winapps" + $SUDO rm -f "${BIN_PATH}/winapps-setup" + + # Remove WinApps configuration data, temporary files and logs. + rm -rf "$USER_APPDATA_PATH" + + # Remove application icons and shortcuts. + $SUDO rm -rf "$APPDATA_PATH" + + # Store '.desktop' files containing "${BIN_PATH}/winapps" in an array, returning an empty array if no such files exist. + readarray -t WINAPPS_DESKTOP_FILES < <(grep -l -d skip "${BIN_PATH}/winapps" "${APP_PATH}/"* 2>/dev/null || true) + + # Remove each '.desktop' file. + for DESKTOP_FILE_PATH in "${WINAPPS_DESKTOP_FILES[@]}"; do + # Trim leading and trailing whitespace from '.desktop' file path. + DESKTOP_FILE_PATH=$(echo "$DESKTOP_FILE_PATH" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + + # Extract the file name. + DESKTOP_FILE_NAME=$(basename "$DESKTOP_FILE_PATH" | sed 's/\.[^.]*$//') + + # Print feedback. + echo -n "Removing '.desktop' file for '${DESKTOP_FILE_NAME}'... " + + # Delete the file. + $SUDO rm "$DESKTOP_FILE_PATH" + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" + done + + # Store the paths of bash scripts calling 'WinApps' to launch specific applications in an array, returning an empty array if no such files exist. + readarray -t WINAPPS_APP_BASH_SCRIPTS < <(grep -l -d skip "${BIN_PATH}/winapps" "${BIN_PATH}/"* 2>/dev/null || true) + + # Remove each bash script. + for BASH_SCRIPT_PATH in "${WINAPPS_APP_BASH_SCRIPTS[@]}"; do + # Trim leading and trailing whitespace from bash script path. + BASH_SCRIPT_PATH=$(echo "$BASH_SCRIPT_PATH" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + + # Extract the file name. + BASH_SCRIPT_NAME=$(basename "$BASH_SCRIPT_PATH" | sed 's/\.[^.]*$//') + + # Print feedback. + echo -n "Removing bash script for '${BASH_SCRIPT_NAME}'... " + + # Delete the file. + $SUDO rm "$BASH_SCRIPT_PATH" + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" + done + + # Print caveats. + echo -e "\n${INFO_TEXT}Please note that your WinApps configuration and the WinApps source code were not removed.${CLEAR_TEXT}" + echo -e "${INFO_TEXT}You can remove these manually by running:${CLEAR_TEXT}" + echo -e "${COMMAND_TEXT}rm -r $(dirname "$CONFIG_PATH")${CLEAR_TEXT}" + echo -e "${COMMAND_TEXT}rm -r ${SOURCE_PATH}${CLEAR_TEXT}\n" + + # Print feedback. + echo -e "${SUCCESS_TEXT}UNINSTALLATION COMPLETE.${CLEAR_TEXT}" +} + +### SEQUENTIAL LOGIC ### +# Welcome the user. +echo -e "${BOLD_TEXT}\ +################################################################################ +# # +# WinApps Install Wizard # +# # +################################################################################ +${CLEAR_TEXT}" + +# Check dependencies for the script. +waCheckScriptDependencies + +# Source the contents of 'inquirer.sh'. +waGetInquirer + +# Sanitise and parse the user input. +waCheckInput "$@" + +# Configure paths and permissions. +waConfigurePathsAndPermissions + +# Get the source code +waGetSourceCode + +# Install or uninstall WinApps. +if [ "$OPT_UNINSTALL" -eq 1 ]; then + waUninstall +else + waInstall +fi + +exit 0 diff --git a/.config/winapps/winapps.conf b/.config/winapps/winapps.conf new file mode 100644 index 0000000..617d16a --- /dev/null +++ b/.config/winapps/winapps.conf @@ -0,0 +1,139 @@ +################################## +# WINAPPS CONFIGURATION FILE # +################################## + +# INSTRUCTIONS +# - Leading and trailing whitespace are ignored. +# - Empty lines are ignored. +# - Lines starting with '#' are ignored. +# - All characters following a '#' are ignored. + +# [WINDOWS USERNAME] +RDP_USER="paul" + +# [WINDOWS PASSWORD] +# NOTES: +# - If using FreeRDP v3.9.0 or greater, you *have* to set a password +RDP_PASS="nicerdicer" + +# [WINDOWS DOMAIN] +# DEFAULT VALUE: '' (BLANK) +RDP_DOMAIN="" + +# [WINDOWS IPV4 ADDRESS] +# NOTES: +# - If using 'libvirt', 'RDP_IP' will be determined by WinApps at runtime if left unspecified. +# DEFAULT VALUE: +# - 'docker': '127.0.0.1' +# - 'podman': '127.0.0.1' +# - 'libvirt': '' (BLANK) +RDP_IP="127.0.0.1" + +# [VM NAME] +# NOTES: +# - Only applicable when using 'libvirt' +# - The libvirt VM name must match so that WinApps can determine VM IP, start the VM, etc. +# DEFAULT VALUE: 'RDPWindows' +VM_NAME="RDPWindows" + +# [WINAPPS BACKEND] +# DEFAULT VALUE: 'docker' +# VALID VALUES: +# - 'docker' +# - 'podman' +# - 'libvirt' +# - 'manual' +WAFLAVOR="docker" + +# [DISPLAY SCALING FACTOR] +# NOTES: +# - If an unsupported value is specified, a warning will be displayed. +# - If an unsupported value is specified, WinApps will use the closest supported value. +# DEFAULT VALUE: '100' +# VALID VALUES: +# - '100' +# - '140' +# - '180' +RDP_SCALE="100" + +# [MOUNTING REMOVABLE PATHS FOR FILES] +# NOTES: +# - By default, `udisks` (which you most likely have installed) uses /run/media for mounting removable devices. +# This improves compatibility with most desktop environments (DEs). +# ATTENTION: The Filesystem Hierarchy Standard (FHS) recommends /media instead. Verify your system's configuration. +# - To manually mount devices, you may optionally use /mnt. +# REFERENCE: https://wiki.archlinux.org/title/Udisks#Mount_to_/media +REMOVABLE_MEDIA="/run/media" + +# [ADDITIONAL FREERDP FLAGS & ARGUMENTS] +# NOTES: +# - You can try adding /network:lan to these flags in order to increase performance, however, some users have faced issues with this. +# DEFAULT VALUE: '/cert:tofu /sound /microphone +home-drive' +# VALID VALUES: See https://github.com/awakecoding/FreeRDP-Manuals/blob/master/User/FreeRDP-User-Manual.markdown +RDP_FLAGS="/cert:tofu /sound /microphone +home-drive" + +# [DEBUG WINAPPS] +# NOTES: +# - Creates and appends to ~/.local/share/winapps/winapps.log when running WinApps. +# DEFAULT VALUE: 'true' +# VALID VALUES: +# - 'true' +# - 'false' +DEBUG="true" + +# [AUTOMATICALLY PAUSE WINDOWS] +# NOTES: +# - This is currently INCOMPATIBLE with 'docker' and 'manual'. +# - See https://github.com/dockur/windows/issues/674 +# DEFAULT VALUE: 'off' +# VALID VALUES: +# - 'on' +# - 'off' +AUTOPAUSE="off" + +# [AUTOMATICALLY PAUSE WINDOWS TIMEOUT] +# NOTES: +# - This setting determines the duration of inactivity to tolerate before Windows is automatically paused. +# - This setting is ignored if 'AUTOPAUSE' is set to 'off'. +# - The value must be specified in seconds (to the nearest 10 seconds e.g., '30', '40', '50', etc.). +# - For RemoteApp RDP sessions, there is a mandatory 20-second delay, so the minimum value that can be specified here is '20'. +# - Source: https://techcommunity.microsoft.com/t5/security-compliance-and-identity/terminal-services-remoteapp-8482-session-termination-logic/ba-p/246566 +# DEFAULT VALUE: '300' +# VALID VALUES: >=20 +AUTOPAUSE_TIME="300" + +# [FREERDP COMMAND] +# NOTES: +# - WinApps will attempt to automatically detect the correct command to use for your system. +# DEFAULT VALUE: '' (BLANK) +# VALID VALUES: The command required to run FreeRDPv3 on your system (e.g., 'xfreerdp', 'xfreerdp3', etc.). +FREERDP_COMMAND="" + +# [TIMEOUTS] +# NOTES: +# - These settings control various timeout durations within the WinApps setup. +# - Increasing the timeouts is only necessary if the corresponding errors occur. +# - Ensure you have followed all the Troubleshooting Tips in the error message first. + +# PORT CHECK +# - The maximum time (in seconds) to wait when checking if the RDP port on Windows is open. +# - Corresponding error: "NETWORK CONFIGURATION ERROR" (exit status 13). +# DEFAULT VALUE: '5' +PORT_TIMEOUT="5" + +# RDP CONNECTION TEST +# - The maximum time (in seconds) to wait when testing the initial RDP connection to Windows. +# - Corresponding error: "REMOTE DESKTOP PROTOCOL FAILURE" (exit status 14). +# DEFAULT VALUE: '30' +RDP_TIMEOUT="30" + +# APPLICATION SCAN +# - The maximum time (in seconds) to wait for the script that scans for installed applications on Windows to complete. +# - Corresponding error: "APPLICATION QUERY FAILURE" (exit status 15). +# DEFAULT VALUE: '60' +APP_SCAN_TIMEOUT="60" + +# WINDOWS BOOT +# - The maximum time (in seconds) to wait for the Windows VM to boot if it is not running, before attempting to launch an application. +# DEFAULT VALUE: '120' +BOOT_TIMEOUT="120" \ No newline at end of file diff --git a/.local/share/darkman/gtk-theme.py b/.local/share/darkman/gtk-theme.py new file mode 100755 index 0000000..6e22a5a --- /dev/null +++ b/.local/share/darkman/gtk-theme.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +import subprocess +from argparse import ArgumentParser + +if __name__ == "__main__": + parser = ArgumentParser(description="GTK theme switcher") + parser.add_argument( + "mode", + help="The mode to switch the GTK theme to (e.g., 'dark' or 'light')", + ) + args = parser.parse_args() + + if args.mode not in ["dark", "light"]: + print("Invalid mode. Please choose 'dark' or 'light'.") + exit(1) + + gtk_theme = "Adwaita-dark" if args.mode == "dark" else "Adwaita" + color_scheme = "prefer-dark" if args.mode == "dark" else "prefer-light" + + # Set the GTK theme using gsettings + subprocess.run( + ["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", gtk_theme], + check=True, + ) + subprocess.run( + ["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", color_scheme], + check=True, + ) + diff --git a/.local/share/darkman/vscode-theme.py b/.local/share/darkman/vscode-theme.py new file mode 100755 index 0000000..b9326e2 --- /dev/null +++ b/.local/share/darkman/vscode-theme.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +import re +from argparse import ArgumentParser +from pathlib import Path + +if __name__ == "__main__": + parser = ArgumentParser(description="VS Code theme switcher") + parser.add_argument( + "mode", + help="The mode to switch the VS Code theme to (e.g., 'dark' or 'light')", + ) + args = parser.parse_args() + + if args.mode not in ["dark", "light"]: + print("Invalid mode. Please choose 'dark' or 'light'.") + exit(1) + + settings = Path.home() / ".config/Code/User/settings.json" + + if not settings.exists(): + print(f"VS Code settings file not found at {settings}") + exit(1) + + # We just replace the theme name in the settings file using regex, + # this way we don't have to worry about jsonc parsing. + theme = "Monokai Pro" if args.mode == "dark" else "Monokai Pro Light" + + with settings.open("r") as f: + content = f.read() + + new_content = re.sub( + r'"workbench\.colorTheme"\s*:\s*".*?"', + f'"workbench.colorTheme": "{theme}"', + content, + ) + + with settings.open("w") as f: + f.write(new_content) \ No newline at end of file diff --git a/.zshrc b/.zshrc index dedfebe..a3535f5 100644 --- a/.zshrc +++ b/.zshrc @@ -142,13 +142,14 @@ alias open="xdg-open" alias lg="lazygit" alias ccat="pygmentize -g -O style=monokai,linenos=1" alias rm="echo Use the full path i.e. '/bin/rm', consider using: trash" -alias explain="gh copilot explain" +alias explain="copilot explain" alias csv="csvlens" alias wstmux="cd ~/code/wasteside/app && tmux new -A -s wasteside" alias R="R --no-save" + # Python tkinter stuff export PATH="/usr/local/opt/tcl-tk/bin:$PATH" @@ -158,6 +159,8 @@ export PATH="/usr/local/opt/tcl-tk/bin:$PATH" # ws shortcuts alias wsproddb="psql -h ep-shiny-brook-a23l5gzv-pooler.eu-central-1.aws.neon.tech -d neondb -U neondb_owner" alias wsdevdb="psql -h ep-bitter-queen-a2jgwcaj-pooler.eu-central-1.aws.neon.tech -d verceldb -U default" +alias wsrecupdb="psql -h ep-icy-dew-aggbrr4l-pooler.c-2.eu-central-1.aws.neon.tech -d neondb -U neondb_owner" +alias wsrecupdevdb="psql -h ep-broad-base-ag8vy0hl-pooler.c-2.eu-central-1.aws.neon.tech -d neondb -U neondb_owner" # Fix ls colors for synology directories LS_COLORS+=':ow=01;33' @@ -170,6 +173,8 @@ HISTFILE=~/.histfile HISTSIZE=10000 SAVEHIST=10000 +export XDG_DATA_DIRS="$HOME/.local/share:$XDG_DATA_DIRS" + # Android tools export ANDROID_SDK_ROOT="$HOME/.local/opt/android-sdk" export ANDROID_HOME="$ANDROID_SDK_ROOT"