diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f570dd9..811e1b4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: remove-crlf - id: forbid-tabs - id: remove-tabs - args: [ --whitespaces-count, "2" ] + args: [ --whitespaces-count, "4" ] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 @@ -43,7 +43,8 @@ repos: - repo: https://github.com/scop/pre-commit-shfmt rev: v3.8.0-1 hooks: - - id: shfmt + - id: shfmt + args: ["-i", "4", "-ci", "-s"] - repo: https://github.com/shellcheck-py/shellcheck-py rev: v0.10.0.1 diff --git a/README.md b/README.md index a86767b..acb2e1c 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,121 @@ # WinApps +*The WinApps project, forked from Fmstrat's [original repository](https://github.com/Fmstrat/winapps).* -The WinApps main project, [originally created by Fmstrat](https://github.com/Fmstrat/winapps) +Run Windows applications (including [Microsoft 365](https://www.microsoft365.com/) and [Adobe Creative Cloud](https://www.adobe.com/creativecloud.html)) on GNU+Linux with `KDE` or `GNOME`, integrated seamlessly as if they were native to the OS. -Run Windows apps such as Microsoft Office/Adobe in Linux (Ubuntu/Fedora) and GNOME/KDE as if they were a part of the native OS, -including Nautilus integration for right-clicking on files of specific mime types to open them. +WinApps Demonstration Animation. - +## Underlying Mechanism +WinApps works by: +1. Running Windows in a `Docker` or `libvirt + KVM/QEMU` virtual machine (deprecated). +2. Querying Windows for all installed applications. +3. Creating shortcuts to selected Windows applications on the host GNU/Linux OS. +4. Using [`FreeRDP`](https://www.freerdp.com/) as a backend to seamlessly render Windows applications alongside GNU/Linux applications. -## How it works +## Additional Features +- The GNU/Linux `/home` directory is accessible within Windows via the `\\tsclient\home` mount. +- Integration with `Nautilus`, allowing you to right-click files to open them with specific Windows applications based on the file MIME type. -WinApps was created as an easy, one-command way to include apps running inside a VM (or on any RDP server) directly into GNOME as if they were native applications. WinApps works by: +## Supported Applications +**WinApps supports *ALL* Windows applications.** -- Running a Windows RDP server in a background VM container -- Checking the RDP server for installed applications such as Microsoft Office -- If those programs are installed, it creates shortcuts leveraging FreeRDP for both the CLI and the GNOME tray -- Files in your home directory are accessible via the `\\tsclient\home` mount inside the VM -- You can right-click on any files in your home directory to open with an application, too +Universal application support is achieved by: +1. Scanning Windows for any officially supported applications (list below). +2. Scanning Windows for any other `.exe` files listed within the Windows Registry. -## Currently supported applications +Officially supported applications benefit from high-resolution icons and pre-populated MIME types. This enables file managers to determine which Windows applications should open files based on file extensions. Icons for other detected applications are pulled from `.exe` files. -### WinApps supports **_ANY_** installed application on your system. +Contributing to the list of supported applications is encouraged through submission of pull requests! Please help us grow the WinApps community. -It does this by: - -1. Scanning your system for the officially configured applications (below) -2. Scanning your system for any other EXE files with install records in the Windows Registry - -Any officially configured applications will have support for high-resolution icons and mime types for automatically detecting what files can be opened by each application. Any other detected executable files will leverage the icons pulled from the EXE. - -Note: The officially configured application list below is fueled by the community, and therefore some apps may be untested by the WinApps team. +*Please note that the provided list of officially supported applications is community-driven. As such, some applications may not be tested and verified by the WinApps team.* +### Officially Supported Applications - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Adobe Acrobat Pro
(X)
Adobe Acrobat Reader
(DC)
Adobe After Effects
(CC)
Adobe Audition
(CC)
Adobe Bridge
(CS6, CC)
Adobe Creative Cloud
(CC)
Adobe Illustrator
(CC)
Adobe InDesign
(CC)
Adobe Lightroom
(CC)
Command Prompt
(cmd.exe)
Explorer
(File Manager)
Internet Explorer
(11)
Microsoft Access
(2016, 2019, o365)
Microsoft Excel
(2016, 2019, o365)
Microsoft Word
(2016, 2019, o365)
Microsoft OneNote
(2016, 2019, o365)
Microsoft Outlook
(2016, 2019, o365)
Microsoft PowerPoint
(2016, 2019, o365)
Microsoft Publisher
(2016, 2019, o365)
PowerShell
Windows
(Full RDP session)
  
Adobe Acrobat Pro
(X)
Adobe After Effects
(CC)
Adobe Audition
(CC)
Adobe Bridge
(CS6, CC)
Adobe Creative Cloud
(CC)
Adobe Illustrator
(CC)
Adobe InDesign
(CC)
Adobe Lightroom
(CC)
Command Prompt
(cmd.exe)
Explorer
(File Manager)
Internet Explorer
(11)
Microsoft Access
(2016, 2019, o365)
Microsoft Excel
(2016, 2019, o365)
Microsoft Word
(2016, 2019, o365)
Microsoft OneNote
(2016, 2019, o365)
Microsoft Outlook
(2016, 2019, o365)
Microsoft PowerPoint
(2016, 2019, o365)
Microsoft Publisher
(2016, 2019, o365)
PowerShellWindows
(Full RDP session)
## Installation +### Step 1: Configure a Windows VM +The optimal choice for running a Windows VM as a subsystem for WinApps is `Docker`. `Docker` facilitates automated installation processes while leveraging a `KVM/QEMU` backend. Despite continuing to provide documentation for configuring a Windows VM using `libvirt` and `virt-manager`, this method is now considered deprecated. -### Step 1: Set up a Windows virtual machine +The following guides are available: +- [Creating a Windows VM with `Docker`](docs/docker.md) +- [Creating a Windows VM with `virt-manager`](docs/KVM.md) (Deprecated) -The best solution for running a VM as a subsystem for WinApps would be Docker. -Docker allows automizing the installation process and still uses KVM/QEMU under the hood. -We still provide the outdated KVM install instructions. -To set up the VM for WinApps, follow this guide: +If you already have a Windows VM or server you wish to use with WinApps, you will need to merge `install/RDPApps.reg` into the Windows Registry. -- [Creating a virtual machine with Docker](docs/docker.md) -- [Creating a virtual machine in KVM (outdated)](docs/KVM.md) +### Step 2: Clone WinApps Repository and Dependencies +1. Clone the WinApps GitHub repository. + ```bash + git clone https://github.com/winapps-org/winapps.git && cd winapps + ``` -If you already have a virtual machine or server you wish to use with WinApps, you will need to merge `install/RDPApps.reg` into the VM's Windows Registry. +2. Install the required dependencies. + - Debian/Ubuntu: + ```bash + sudo apt install -y dialog freerdp3-x11 + ``` + - Fedora/RHEL: + ```bash + sudo dnf install -y dialog freerdp + ``` + - Arch Linux: + ```bash + sudo pacman -Syu --needed -y dialog freerdp + ``` + - Gentoo Linux: + ```bash + sudo emerge --ask=n sys-libs/dialog net-misc/freerdp:3 + ``` -### Step 2: Download the repo and prerequisites - -To get things going, use: +Please note that WinApps requires `FreeRDP` version 3 or later. If not available for your distribution through your package manager, you can install the [Flatpak](https://flathub.org/apps/com.freerdp.FreeRDP). ```bash -sudo apt install -y freerdp3-x11 -git clone https://github.com/winapps-org/winapps.git -cd winapps +flatpak install flathub com.freerdp.FreeRDP +sudo flatpak override --filesystem=home com.freerdp.FreeRDP # To use `+home-drive` ``` -> [!note] -> Requires FreeRDP 3.0.0 or later. -> If not included in your distribution, you can download the Flatpak from here: https://github.com/FreeRDP/FreeRDP/wiki/Prebuilds - -### Step 3: Creating your WinApps configuration file - -You will need to create a `~/.config/winapps/winapps.conf` configuration file with the following information in it: - +### Step 3: Create a WinApps Configuration File +Create a configuration file at `~/.config/winapps/winapps.conf` containing the following: ```bash RDP_USER="MyWindowsUser" RDP_PASS="MyWindowsPassword" @@ -121,95 +128,52 @@ RDP_PASS="MyWindowsPassword" #FREERDP_COMMAND="xfreerdp" ``` -The username and password should be a full user account and password, such as the one created when setting up Windows -or a domain user. It can't be a user/PIN combination as those aren't valid for RDP access. +`RDP_USER` and `RDP_PASS` must correspond to a complete Windows user account and password, such as those created during Windows setup or for a domain user. User/PIN combinations are not valid for RDP access. -Options: - -- When using a pre-existing non-KVM RDP server, you must use the `RDP_IP` to specify its location -- If you're running a VM in KVM with NAT enabled, leave `RDP_IP` commented out and WinApps will auto-detect the right local IP -- For domain users, you can uncomment and change `RDP_DOMAIN` -- On high-resolution (UHD) displays, you can set `RDP_SCALE` to the scale you would like [100|140|160|180] -- To add flags to the FreeRDP call, such as `/audio-mode:1` to pass in a mic, use the `RDP_FLAGS` configuration option -- For multi-monitor setups, you can try enabling `MULTIMON`, however, if you get a black screen (FreeRDP bug) you will need to revert +#### Configuration Options Explained +- When using a pre-existing non-KVM RDP server, you must use `RDP_IP` to specify the location of the Windows server. +- If running a Windows VM in KVM with NAT enabled, leave `RDP_IP` commented out and WinApps will auto-detect the local IP address for the VM. +- For domain users, you can uncomment and change `RDP_DOMAIN`. +- On high-resolution (UHD) displays, you can set `RDP_SCALE` to the scale you would like to use [100|140|160|180]. +- To add flags to the FreeRDP call, such as `/audio-mode:1` to pass in a microphone, uncomment and use the `RDP_FLAGS` configuration option. +- For multi-monitor setups, you can try enabling `MULTIMON`. A FreeRDP bug may result in a black screen however, in which case you should revert this change. - If you enable `DEBUG`, a log will be created on each application start in `~/.local/share/winapps/winapps.log` -- If you're on a system, where the command for freerdp is not xfreerdp, change `FREERDP_COMMAND` to it. - -### Step 4: Run the WinApps installer - -Lastly, check if FreeRDP can connect with: - -``` -bin/winapps check -``` - -You will see output from FreeRDP, as well as potentially have to accept the initial certificate. After that, a Windows Explorer window should pop up. You can close this window and press `Ctrl-C` to cancel out of FreeRDP. - -If this step fails, try restarting the VM, or your problem could be related to: - -- You need to accept the security cert the first time you connect (with 'check') -- Not enabling RDP in the Windows VM -- Not being able to connect to the IP of the VM -- Incorrect user credentials in `~/.config/winapps/winapps.conf` -- Not merging `install/RDPApps.reg` into the VM - -Then the final step is to run the installer which will prompt you to a system or user install: +- If using a system on which the FreeRDP command is not `xfreerdp`, the correct command can be specified using `FREERDP_COMMAND`. +### Step 4: Run the WinApps Installer +Run the WinApps installer. ```bash ./installer.sh ``` -This will take you through the following process: +A list of supported additional arguments can be accessed by running `./installer.sh --help`. -## Adding pre-defined applications +## Adding Additional Pre-defined Applications +Adding your own applications with custom icons and MIME types to the installer is easy. Simply copy one of the application configurations in the `apps` folder located within the WinApps repository, and: +1. Modify the name and variables to reflect the appropriate/desired values for your application. +2. Replace `icon.svg` with an SVG for your application (ensuring the icon is appropriately licensed). +3. Remove and reinstall WinApps. +4. (Optional, but strongly encouraged) Submit a pull request to add your application to WinApps as an officially supported application once you have tested your configuration files to verify functionality. -Adding applications with custom icons and mime types to the installer is easy. Simply copy one of the application configurations in the `apps` folder, and: - -- Edit the variables for the application -- Replace the `icon.svg` with an SVG for the application (appropriately licensed) -- Re-run the installer -- Submit a Pull Request to add it to WinApps officially - -When running the installer, it will check for if any configured apps are installed, and if they are, -it will create the appropriate shortcuts on the host OS. - -## Running applications manually - -WinApps offers a manual mode for running applications that aren't configured. This is completed with the `manual` flag. -Executables that are in the path don't require full path definition. +## Running Applications Manually +WinApps offers a manual mode for running applications that were not configured by the WinApps installer. This is completed with the `manual` flag. Executables that are in the Windows PATH do not require full path definition. ```bash ./bin/winapps manual "C:\my\directory\executableNotInPath.exe" ./bin/winapps manual executableInPath.exe ``` -## Checking for new application support - -The installer can be run multiple times, so simply run the below again, and it will remove any current installations and update for the latest applications. - -```bash -./installer.sh -``` - -## Optional installer command line arguments - -The following optional commands can be used to manage your application configurations without prompts: - -```bash -./installer.sh --user # Configure applications for the current user -./installer.sh --system # Configure applications for the entire system -./installer.sh --user --uninstall # Remove all configured applications for the current user -./installer.sh --system --uninstall # Remove all configured applications for the entire system -./installer.sh --user --setupAllOfficiallySupportedApps # Configures all officially supported applications for the current user -./installer.sh --system --setupAllOfficiallySupportedApps # Configures all officially supported applications for the entire system -``` +## Updating WinApps +The installer can be run multiple times. To update your installation of WinApps: +1. Run the WinApps installer to remove WinApps from your system. +2. Pull the latest changes from the WinApps GitHub repository. +3. Re-install WinApps using the WinApps installer. ## Shout-outs - -- Some icons pulled from - - Fluent UI React - Icons under [MIT License](https://github.com/Fmstrat/fluent-ui-react/blob/master/LICENSE.md) - - Fluent UI - Icons under [MIT License](https://github.com/Fmstrat/fluentui/blob/master/LICENSE) with [restricted use](https://static2.sharepointonline.com/files/fabric/assets/microsoft_fabric_assets_license_agreement_nov_2019.pdf) - - PKief's VSCode Material Icon Theme - Icons under [MIT License](https://github.com/Fmstrat/vscode-material-icon-theme/blob/master/LICENSE.md) - - DiemenDesign's LibreICONS - Icons under [MIT License](https://github.com/Fmstrat/LibreICONS/blob/master/LICENSE) +Some icons used for the officially supported applications were sourced from: +- Fluent UI React - Icons under [MIT License](https://github.com/Fmstrat/fluent-ui-react/blob/master/LICENSE.md) +- Fluent UI - Icons under [MIT License](https://github.com/Fmstrat/fluentui/blob/master/LICENSE) with [restricted use](https://static2.sharepointonline.com/files/fabric/assets/microsoft_fabric_assets_license_agreement_nov_2019.pdf) +- PKief's VSCode Material Icon Theme - Icons under [MIT License](https://github.com/Fmstrat/vscode-material-icon-theme/blob/master/LICENSE.md) +- DiemenDesign's LibreICONS - Icons under [MIT License](https://github.com/Fmstrat/LibreICONS/blob/master/LICENSE) diff --git a/bin/winapps b/bin/winapps index 228f397..7504ddf 100755 --- a/bin/winapps +++ b/bin/winapps @@ -1,209 +1,329 @@ #!/usr/bin/env bash -if [ ! -f "$HOME/.config/winapps/winapps.conf" ] && [ ! -f "$HOME/.winapps" ]; then - echo "You need to create a ~/.config/winapps/winapps.conf configuration. Exiting..." - exit -fi +### GLOBAL CONSTANTS ### +# ANSI ESCAPE SEQUENCES +readonly ERROR_TEXT="\033[1;41;37m" # Bold + White + Red Background +readonly CLEAR_TEXT="\033[0m" # Clear -DIR="$(dirname "$(readlink -f "$0")")" -RUN="$(date)-$RANDOM" +# ERROR CODES +readonly EC_MISSING_CONFIG=1 +readonly EC_MISSING_FREERDP=2 +readonly EC_NOT_IN_GROUP=3 +readonly EC_VM_NOT_RUNNING=4 +readonly EC_VM_NO_IP=5 +readonly EC_VM_BAD_PORT=6 +readonly EC_UNSUPPORTED_APP=7 -if [ ! -d "$HOME/.local/share/winapps" ]; then - mkdir -p "$HOME/.local/share/winapps" -fi +# PATHS +readonly APPDATA_PATH="${HOME}/.local/share/winapps" +readonly SYS_APP_PATH="/usr/local/share/winapps" +readonly LASTRUN_PATH="${APPDATA_PATH}/lastrun" +readonly LOG_PATH="${APPDATA_PATH}/winapps.log" +readonly CONFIG_PATH="${HOME}/.config/winapps/winapps.conf" +# shellcheck disable=SC2155 # Silence warnings regarding masking return values through simultaneous declaration and assignment. +readonly SCRIPT_DIR_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +# OTHER +readonly VM_NAME="RDPWindows" +readonly RDP_PORT=3389 +# shellcheck disable=SC2155 # Silence warnings regarding masking return values through simultaneous declaration and assignment. +readonly RUN="$(date)-${RANDOM}" + +### GLOBAL VARIABLES ### +# WINAPPS CONFIGURATION FILE +RDP_USER="" +RDP_PASS="" +RDP_DOMAIN="" +RDP_IP="" +RDP_FLAGS="" +FREERDP_COMMAND="" RDP_SCALE=100 +MULTIMON="false" +DEBUG="true" +MULTI_FLAG="" -if [ -f "$HOME/.config/winapps/winapps.conf" ]; then - # shellcheck source=/dev/null - . "$HOME/.config/winapps/winapps.conf" -else - # shellcheck source=/dev/null - . "$HOME/.winapps" -fi +### FUNCTIONS ### +# Name: 'waThrowExit' +# Role: Throw an error message and exit the script. +function waThrowExit() { + # Declare variables. + local ERR_CODE="$1" + # Throw error. + case "$ERR_CODE" in + "$EC_MISSING_CONFIG") + # Missing WinApps configuration file. + dprint "ERROR: MISSING WINAPPS CONFIGURATION FILE. EXITING." + echo -e "${ERROR_TEXT}ERROR: MISSING WINAPPS CONFIGURATION FILE.${CLEAR_TEXT}" + echo "Please create a WinApps configuration file at '${CONFIG_PATH}'". + ;; + "$EC_MISSING_FREERDP") + dprint "ERROR: FREERDP VERSION 3 IS NOT INSTALLED. EXITING." + echo -e "${ERROR_TEXT}ERROR: FREERDP VERSION 3 IS NOT INSTALLED.${CLEAR_TEXT}" + ;; + "$EC_NOT_IN_GROUP") + dprint "ERROR: USER NOT PART OF REQUIRED GROUPS. EXITING." + echo -e "${ERROR_TEXT}ERROR: USER NOT PART OF REQUIRED GROUPS.${CLEAR_TEXT}" + echo "Please run:" + echo " sudo usermod -a -G libvirt $(whoami)" + echo " sudo usermod -a -G kvm $(whoami)" + ;; + "$EC_VM_NOT_RUNNING") + dprint "ERROR: VM NOT RUNNING. EXITING." + echo -e "${ERROR_TEXT}ERROR: VM NOT RUNNING.${CLEAR_TEXT}" + echo "Please ensure the Windows VM is powered on." + ;; + "$EC_VM_NO_IP") + dprint "ERROR: VM UNREACHABLE. EXITING." + echo -e "${ERROR_TEXT}ERROR: VM UNREACHABLE.${CLEAR_TEXT}" + echo "Please ensure the Windows VM is assigned an IP address." + ;; + "$EC_VM_BAD_PORT") + dprint "ERROR: RDP PORT CLOSED. EXITING." + echo -e "${ERROR_TEXT}ERROR: RDP PORT CLOSED.${CLEAR_TEXT}" + echo "Please ensure Remote Desktop is correctly configured on the Windows VM." + ;; + "$EC_UNSUPPORTED_APP") + dprint "ERROR: APPLICATION NOT FOUND. EXITING." + echo -e "${ERROR_TEXT}ERROR: APPLICATION NOT FOUND.${CLEAR_TEXT}" + echo "Please ensure the program is correctly configured as an officially supported application." + ;; + esac + + # Provide generic advice. + echo "Check the WinApps project README for more information." + + # Terminate the script. + echo "Exiting with status '${ERR_CODE}'." + exit "$ERR_CODE" +} + +# Name: 'dprint' +# Role: Conditionally print debug messages to a log file, creating it if it does not exist. function dprint() { - if [ "$DEBUG" = "true" ]; then - echo "[$RUN] $1" >>"$HOME/.local/share/winapps/winapps.log" + [ "$DEBUG" = "true" ] && echo "[$RUN] $1" >>"$LOG_PATH" +} + +# Name: 'waLoadConfig' +# Role: Load the variables within the WinApps configuration file. +function waLoadConfig() { + # Load WinApps configuration file. + if [ -f "$CONFIG_PATH" ]; then + # shellcheck source=/dev/null # Exclude WinApps configuration file from being checked by ShellCheck. + source "$CONFIG_PATH" + else + waThrowExit $EC_MISSING_CONFIG + fi + + # Update 'MULTI_FLAG' based on 'MULTIMON'. + MULTI_FLAG=$([[ $MULTIMON == "true" ]] && echo "/multimon" || echo "+span") + + # Append additional flags or parameters to FreeRDP. + [[ -n $RDP_FLAGS ]] && FREERDP_COMMAND="${FREERDP_COMMAND} ${RDP_FLAGS}" +} + +# Name: 'waLastRun' +# Role: Determine the last time this script was run. +function waLastRun() { + # Declare variables. + local LAST_RUN_UNIX_TIME=0 + local CURR_RUN_UNIX_TIME=0 + + # Store the time this script was run last as a unix timestamp. + if [ -f "$LASTRUN_PATH" ]; then + LAST_RUN_UNIX_TIME=$(stat -t -c %Y "$LASTRUN_PATH") + dprint "LAST_RUN: ${LAST_RUN_UNIX_TIME}" + fi + + # Update the file modification time with the current time. + touch "$LASTRUN_PATH" + CURR_RUN_UNIX_TIME=$(stat -t -c %Y "$LASTRUN_PATH") + dprint "THIS_RUN: ${CURR_RUN_UNIX_TIME}" +} + +function waGetFreeRDPCommand() { + # Attempt to set a FreeRDP command if the command variable is empty. + if [ -z "$FREERDP_COMMAND" ]; then + # Check for 'xfreerdp'. + 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*' | cut -d'.' -f1) + if [[ $FREERDP_MAJOR_VERSION =~ ^[0-9]+$ ]] && ((FREERDP_MAJOR_VERSION >= 3)); then + FREERDP_COMMAND="xfreerdp" + fi + # Check for 'xfreerdp3'. + elif 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*' | cut -d'.' -f1) + if [[ $FREERDP_MAJOR_VERSION =~ ^[0-9]+$ ]] && ((FREERDP_MAJOR_VERSION >= 3)); then + FREERDP_COMMAND="xfreerdp3" + fi + fi + + # Check for FreeRDP Flatpak (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 + dprint "Using FreeRDP command '${FREERDP_COMMAND}'." + else + waThrowExit "$EC_MISSING_FREERDP" fi } +# Name: 'waCheckGroupMembership' +# Role: Ensures the current user is part of the required groups. +function waCheckGroupMembership() { + # Identify groups the current user belongs to. + # shellcheck disable=SC2155 # Silence warnings regarding masking return values through simultaneous declaration and assignment. + local USER_GROUPS=$(groups "$(whoami)") + + if ! (echo "$USER_GROUPS" | grep -q -E "\blibvirt\b") || ! (echo "$USER_GROUPS" | grep -q -E "\bkvm\b"); then + waThrowExit "$EC_NOT_IN_GROUP" + fi +} + +# Name: 'waCheckVMRunning' +# Role: Throw an error if the Windows VM is not running. +function waCheckVMRunning() { + ! virsh list --state-running --name | grep -q "^${VM_NAME}$" && waThrowExit "$EC_VM_NOT_RUNNING" +} + +# Name: 'waCheckVMContactable' +# Role: Assesses whether the Windows VM can be contacted. +function waCheckVMContactable() { + # Declare variables. + local VM_MAC="" # Stores the MAC address of the Windows VM. + + # Obtain Windows VM IP Address + if [ -z "$RDP_IP" ]; 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=$(arp -n | grep "$VM_MAC" | grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}") # VM IP address. + [ -z "$RDP_IP" ] && waThrowExit "$EC_VM_NO_IP" + fi + + # Check for an open RDP port. + timeout 5 nc -z "$RDP_IP" "$RDP_PORT" &>/dev/null || waThrowExit "$EC_VM_BAD_PORT" +} + +function waRunCommand() { + # Declare variables. + local ICON="" + local FILE_PATH="" + + # Run option. + if [ "$1" = "windows" ]; then + # Open Windows VM. + dprint "WINDOWS" + $FREERDP_COMMAND \ + /d:"$RDP_DOMAIN" \ + /u:"$RDP_USER" \ + /p:"$RDP_PASS" \ + /scale:"$RDP_SCALE" \ + +dynamic-resolution \ + +auto-reconnect \ + +home-drive \ + /wm-class:"Microsoft Windows" \ + /v:"$RDP_IP" &>/dev/null & + elif [ "$1" = "manual" ]; then + # Open specified application. + dprint "MANUAL: ${2}" + $FREERDP_COMMAND \ + /cert:tofu \ + /d:"$RDP_DOMAIN" \ + /u:"$RDP_USER" \ + /p:"$RDP_PASS" \ + /scale:"$RDP_SCALE" \ + +auto-reconnect \ + +clipboard \ + +home-drive \ + -wallpaper \ + +dynamic-resolution \ + "$MULTI_FLAG" \ + /app:program:"$2" \ + /v:"$RDP_IP" &>/dev/null & + else + # Script summoned from right-click menu with officially supported application name plus/minus a file path. + if [ -e "${SCRIPT_DIR_PATH}/../apps/${1}/info" ]; then + # shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck. + source "${SCRIPT_DIR_PATH}/../apps/${1}/info" + ICON="${SCRIPT_DIR_PATH}/../apps/${1}/icon.svg" + elif [ -e "${APPDATA_PATH}/apps/${1}/info" ]; then + # shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck. + source "${APPDATA_PATH}/apps/${1}/info" + ICON="${APPDATA_PATH}/apps/${1}/icon.svg" + elif [ -e "${SYS_APP_PATH}/apps/${1}/info" ]; then + # shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck. + source "${SYS_APP_PATH}/apps/${1}/info" + ICON="${SYS_APP_PATH}/apps/${1}/icon.svg" + else + waThrowExit "$EC_UNSUPPORTED_APP" + fi + + # Check if a file path was specified, and pass this to the application. + if [ -z "$2" ]; then + # No file path specified. + $FREERDP_COMMAND \ + /d:"$RDP_DOMAIN" \ + /u:"$RDP_USER" \ + /p:"$RDP_PASS" \ + /scale:"$RDP_SCALE" \ + +auto-reconnect \ + +clipboard \ + +home-drive \ + -wallpaper \ + +dynamic-resolution \ + "$MULTI_FLAG" \ + /wm-class:"$FULL_NAME" \ + /app:program:"$WIN_EXECUTABLE",icon:"$ICON",name:"$FULL_NAME" \ + /v:"$RDP_IP" &>/dev/null & + else + # Convert path from UNIX to Windows style. + FILE_PATH=$(echo "$2" | sed 's|'"${HOME}"'|\\\\tsclient\\home|;s|/|\\|g;s|\\|\\\\|g') + dprint "UNIX_FILE_PATH: ${2}" + dprint "WINDOWS_FILE_PATH: ${FILE_PATH}" + + $FREERDP_COMMAND \ + /cert:tofu \ + /d:"$RDP_DOMAIN" \ + /u:"$RDP_USER" \ + /p:"$RDP_PASS" \ + /scale:"$RDP_SCALE" \ + +auto-reconnect \ + +clipboard \ + +home-drive \ + -wallpaper \ + +dynamic-resolution \ + "$MULTI_FLAG" \ + /wm-class:"$FULL_NAME" \ + /app:program:"$WIN_EXECUTABLE",icon:"$ICON",name:$"FULL_NAME",cmd:\""$FILE_PATH"\" \ + /v:"$RDP_IP" &>/dev/null & + fi + fi +} + +### MAIN LOGIC ### +#set -x # Enable for debugging. dprint "START" - -if [ -f "$HOME/.local/share/winapps/run" ]; then - LAST_RAN=$(stat -t -c %Y "$HOME/.local/share/winapps/run") - dprint "LAST_RAN:${LAST_RAN}" - touch "$HOME/.local/share/winapps/run" - THIS_RUN=$(stat -t -c %Y "$HOME/.local/share/winapps/run") - dprint "THIS_RUN:$THIS_RUN" - if ((THIS_RUN - LAST_RAN < 2)); then - exit - fi -else - touch "$HOME/.local/share/winapps/run" -fi - -if [ -z "${FREERDP_COMMAND}" ]; then - if command -v xfreerdp &> /dev/null - then - FREERDP_COMMAND="xfreerdp" - elif command -v xfreerdp3 &> /dev/null - then - FREERDP_COMMAND="xfreerdp3" - fi -elif command -v "$FREERDP_COMMAND" &> /dev/null -then - dprint "Using custom freerdp command $FREERDP_COMMAND" -else - echo "You have supplied a custom FreeRDP command, but the command is not available." - exit -fi - -if [ -z "$RDP_IP" ]; then - if groups | grep -vq libvirt; then - echo "You are not a member of the libvirt group. Run the below then reboot." - echo " sudo usermod -a -G libvirt $(whoami)" - echo " sudo usermod -a -G kvm $(whoami)" - exit - fi - if ! virsh list --state-running --name | grep -q '^RDPWindows$'; then - echo "RDPWindows is not running. Please run:" - echo " virsh start RDPWindows" - exit - fi - RDP_IP=$(virsh net-dhcp-leases default | grep "RDPWindows" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}') -fi - -dprint "1:$1" -dprint "2:$2" -# this is just for debug logging anyways -# shellcheck disable=SC2145 -dprint "@:${@}" - -MULTI_FLAG="+span" -if [ "$MULTIMON" = "true" ]; then - MULTI_FLAG="/multimon" -fi - -# Append additional flags or parameters to FreeRDP -if [[ -n "$RDP_FLAGS" ]]; then - FREERDP_COMMAND="$FREERDP_COMMAND $RDP_FLAGS" -fi - -if [ "$1" = "check" ]; then - # Open File Explorer - dprint "CHECK" - COMMAND=( - "${FREERDP_COMMAND}" - "/d:${RDP_DOMAIN}" - "/u:${RDP_USER}" - "/p:${RDP_PASS}" - "/scale:${RDP_SCALE}" - "+auto-reconnect" - "+home-drive" - "-wallpaper" - "+dynamic-resolution" - "${MULTI_FLAG}" - "/app:program:explorer.exe" - "/v:${RDP_IP}" - ) - "${COMMAND[@]}" -elif [ "$1" = "windows" ]; then - # Open Virtual Machine - dprint "WINDOWS" - COMMAND=( - "${FREERDP_COMMAND}" - "/d:${RDP_DOMAIN}" - "/u:${RDP_USER}" - "/p:${RDP_PASS}" - "/scale:${RDP_SCALE}" - "+dynamic-resolution" - "+auto-reconnect" - "+home-drive" - "/wm-class:\"Microsoft Windows\"" - "/v:${RDP_IP}" - ) - # Run the command in the background, redirecting both stdout and stderr to /dev/null - "${COMMAND[@]}" 1>/dev/null 2>&1 & -elif [ "$1" = "manual" ]; then - # Open Specified Application - dprint "MANUAL:${2}" - COMMAND=( - "${FREERDP_COMMAND}" - "/d:${RDP_DOMAIN}" - "/u:${RDP_USER}" - "/p:${RDP_PASS}" - "/scale:${RDP_SCALE}" - "+auto-reconnect" - "+home-drive" - "+dynamic-resolution" - "${MULTI_FLAG}" - "/app:program:${2}" - "/v:${RDP_IP}" - ) - # Run the command in the background, redirecting both stdout and stderr to /dev/null - "${COMMAND[@]}" 1>/dev/null 2>&1 & -elif [ "$1" != "install" ]; then - dprint "DIR:${DIR}" - if [ -e "${DIR}/../apps/$1/info" ]; then - # shellcheck disable=SC1090 - . "${DIR}/../apps/$1/info" - ICON="${DIR}/../apps/$1/icon.svg" - elif [ -e "$HOME/.local/share/winapps/apps/$1/info" ]; then - # shellcheck disable=SC1090 - . "$HOME/.local/share/winapps/apps/$1/info" - ICON="$HOME/.local/share/winapps/apps/$1/icon.svg" - elif [ -e "/usr/local/share/winapps/apps/$1/info" ]; then - # shellcheck disable=SC1090 - . "/usr/local/share/winapps/apps/$1/info" - ICON="/usr/local/share/winapps/apps/$1/icon.svg" - else - echo "You need to run 'installer.sh' first." - exit 1 - fi - if [ -n "$2" ]; then - dprint "HOME:$HOME" - FILE=$(echo "$2" | sed 's|'"$HOME"'|\\\\tsclient\\home|;s|/|\\|g;s|\\|\\\\|g') - dprint "FILE:${FILE}" - # shellcheck disable=SC2140 - COMMAND=( - "${FREERDP_COMMAND}" - "/d:${RDP_DOMAIN}" - "/u:${RDP_USER}" - "/p:${RDP_PASS}" - "/scale:${RDP_SCALE}" - "+auto-reconnect" - "+clipboard" - "+home-drive" - "-wallpaper" - "+dynamic-resolution" - "${MULTI_FLAG}" - "/wm-class:${FULL_NAME}" - "/app:program:${WIN_EXECUTABLE},icon:${ICON},name:${FULL_NAME},cmd:\"${FILE}\"" - "/v:${RDP_IP}" - ) - # Run the command in the background, redirecting both stdout and stderr to /dev/null - echo "${COMMAND[@]}" #1>/dev/null 2>&1 & - else - COMMAND=( - "${FREERDP_COMMAND}" - "/d:${RDP_DOMAIN}" - "/u:${RDP_USER}" - "/p:${RDP_PASS}" - "/scale:${RDP_SCALE}" - "+auto-reconnect" - "+clipboard" - "+home-drive" - "-wallpaper" - "+dynamic-resolution" - "${MULTI_FLAG}" - "/wm-class:${FULL_NAME}" - "/app:program:${WIN_EXECUTABLE},icon:${ICON},name:${FULL_NAME}" - "/v:${RDP_IP}" - ) - # Run the command in the background, redirecting both stdout and stderr to /dev/null - "${COMMAND[@]}" 1>/dev/null 2>&1 & - fi -fi - +dprint "SCRIPT_DIR: ${SCRIPT_DIR_PATH}" +dprint "SCRIPT_ARGS: ${*}" +dprint "HOME_DIR: ${HOME}" +mkdir -p "$APPDATA_PATH" +waLastRun +waLoadConfig +waGetFreeRDPCommand +waCheckGroupMembership +waCheckVMRunning +waCheckVMContactable +waRunCommand "$@" dprint "END" diff --git a/demo/installer.gif b/demo/installer.gif index b152494..5123715 100644 Binary files a/demo/installer.gif and b/demo/installer.gif differ diff --git a/icons/windows.svg b/icons/windows.svg index ea5b408..ec8f033 100644 --- a/icons/windows.svg +++ b/icons/windows.svg @@ -1,3 +1,14 @@ - - + + POO + + + + + + + + + diff --git a/install/inquirer.sh b/install/inquirer.sh index 2b9e962..881079a 100755 --- a/install/inquirer.sh +++ b/install/inquirer.sh @@ -1,835 +1,186 @@ #!/usr/bin/env bash -#======== Based on (then updated) https://raw.githubusercontent.com/tanhauhau/Inquirer.sh/master/dist/inquirer.sh ======== +### GLOBAL CONSTANTS ### +declare -r ANSI_LIGHT_BLUE="\033[1;94m" # Light blue text. +declare -r ANSI_LIGHT_GREEN="\033[92m" # Light green text. +declare -r ANSI_CLEAR_TEXT="\033[0m" # Default text. +declare -r DIALOG_HEIGHT=14 # Height of dialog window. +declare -r TEXT_WIDTH_OFFSET=4 # Offset for fitting title text. +declare -r CHK_OPTION_WIDTH_OFFSET=10 # Offset for fitting options. +declare -r MNU_OPTION_WIDTH_OFFSET=7 # Offset for fitting options. -# License from: https://github.com/kahkhang/Inquirer.sh/blob/master/LICENSE +### FUNCTIONS ### +function inqMenu() { + # DECLARE VARIABLES. + # Variables created from function arguments: + declare DIALOG_TEXT="$1" # Dialog heading. + declare INPUT_OPTIONS_VAR="$2" # Input variable name. + declare RETURN_STRING_VAR="$3" # Output variable name. + declare -n INPUT_OPTIONS="$INPUT_OPTIONS_VAR" # Input array nameref. + declare -n RETURN_STRING="$RETURN_STRING_VAR" # Output string nameref. + # Note: namerefs allow changes made through the nameref to affect the + # referenced variable, even across different scopes like function calls. -# The MIT License (MIT) + # Other variables: + declare TRIMMED_OPTIONS=() # Input array post-trimming. + declare PADDED_OPTIONS=() # Input array with extra white space. + declare DIALOG_OPTIONS=() # Input array for options dialog. + declare DIALOG_WIDTH=0 # Width of dialog window. + declare OPTION_NUMBER=0 # Number of options in dialog window. + declare SELECTED_OPTIONS_STRING="" # Output value from dialog window. -# Copyright (c) 2017 - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -# shellcheck disable=all - -# store the current set options -OLD_SET=$- -set -e - -arrow="$(echo -e '\xe2\x9d\xaf')" -checked="$(echo -e '\xe2\x97\x89')" -unchecked="$(echo -e '\xe2\x97\xaf')" -down_arrow=$(echo -e '\u23f7') -up_arrow=$(echo -e '\u23f6') - -black="$(tput setaf 0)" -red="$(tput setaf 1)" -green="$(tput setaf 2)" -yellow="$(tput setaf 3)" -blue="$(tput setaf 4)" -magenta="$(tput setaf 5)" -cyan="$(tput setaf 6)" -white="$(tput setaf 7)" -bold="$(tput bold)" -normal="$(tput sgr0)" -dim=$'\e[2m' - -print() { - echo "$1" - tput el -} - -join() { - local IFS=$'\n' - local _join_list - eval _join_list=( '"${'${1}'[@]}"' ) - local first=true - for item in ${_join_list[@]}; do - if [ "$first" = true ]; then - printf "%s" "$item" - first=false - else - printf "${2-, }%s" "$item" - fi - done -} - -function gen_env_from_options() { - local IFS=$'\n' - local _indices - local _env_names - local _checkbox_selected - eval _indices=( '"${'${1}'[@]}"' ) - eval _env_names=( '"${'${2}'[@]}"' ) - - for i in $(gen_index ${#_env_names[@]}); do - _checkbox_selected[$i]=false + # MAIN LOGIC. + # Trim leading and trailing white space for each option. + for OPTION in "${INPUT_OPTIONS[@]}"; do + TRIMMED_OPTIONS+=("$(echo "$OPTION" | sed 's/^[ \t]*//;s/[ \t]*$//')") done - for i in ${_indices[@]}; do - _checkbox_selected[$i]=true - done - - for i in $(gen_index ${#_env_names[@]}); do - printf "%s=%s\n" "${_env_names[$i]}" "${_checkbox_selected[$i]}" - done -} - -on_default() { - true; -} - -on_keypress() { - local OLD_IFS - local IFS - local key - OLD_IFS=$IFS - local on_up=${1:-on_default} - local on_down=${2:-on_default} - local on_space=${3:-on_default} - local on_enter=${4:-on_default} - local on_left=${5:-on_default} - local on_right=${6:-on_default} - local on_ascii=${7:-on_default} - local on_backspace=${8:-on_default} - _break_keypress=false - while IFS="" read -rsn1 key; do - case "$key" in - $'\x1b') - read -rsn1 key - if [[ "$key" == "[" ]]; then - read -rsn1 key - case "$key" in - 'A') eval $on_up ;; - 'B') eval $on_down ;; - 'D') eval $on_left ;; - 'C') eval $on_right ;; - esac - fi - ;; - ' ') eval $on_space ' ' ;; - [a-z0-9A-Z\!\#\$\&\+\,\-\.\/\;\=\?\@\[\]\^\_\{\}\~]) eval $on_ascii $key ;; - $'\x7f') eval $on_backspace $key ;; - '') eval $on_enter $key ;; - esac - if [ $_break_keypress = true ]; then - break - fi - done - IFS=$OLD_IFS -} - -gen_index() { - local k=$1 - local l=0 - if [ $k -gt 0 ]; then - for l in $(seq $k) - do - echo "$l-1" | bc - done - fi -} - -cleanup() { - # Reset character attributes, make cursor visible, and restore - # previous screen contents (if possible). - tput sgr0 - tput cnorm - stty echo - - # Restore `set e` option to its orignal value - if [[ $OLD_SET =~ e ]] - then set -e - else set +e - fi -} - -control_c() { - cleanup - exit $? -} - -select_indices() { - local _select_list - local _select_indices - local _select_selected=() - eval _select_list=( '"${'${1}'[@]}"' ) - eval _select_indices=( '"${'${2}'[@]}"' ) - local _select_var_name=$3 - eval $_select_var_name\=\(\) - for i in $(gen_index ${#_select_indices[@]}); do - eval $_select_var_name\+\=\(\""${_select_list[${_select_indices[$i]}]}"\"\) - done -} - -print_checkbox_line_arrow() { - if [ "${_checkbox_selected[$1]}" = true ]; then - printf "${cyan}${arrow}${green}${checked}${normal} ${_checkbox_list[$1]} ${normal}" - else - printf "${cyan}${arrow}${normal}${unchecked} ${_checkbox_list[$1]} ${normal}" - fi -} - -print_checkbox_line() { - if [ "${_checkbox_selected[$1]}" = true ]; then - printf " ${green}${checked}${normal} ${_checkbox_list[$1]} ${normal}" - else - printf " ${normal}${unchecked} ${_checkbox_list[$1]} ${normal}" - fi -} - -# https://www.gnu.org/software/termutils/manual/termutils-2.0/html_chapter/tput_1.html -# http://linuxcommand.org/lc3_adv_tput.php -on_checkbox_input_up2() { - #remove_checkbox_instructions - tput cub "$(tput cols)" - - if [ "${_checkbox_selected[$_current_index]}" = true ]; then - printf " ${green}${checked}${normal} ${_checkbox_list[$_current_index]} ${normal}" - else - printf " ${unchecked} ${_checkbox_list[$_current_index]} ${normal}" - fi - tput el - - if [ $_current_index = 0 ]; then - _current_index=$((${#_checkbox_list[@]}-1)) - tput cud $((${#_checkbox_list[@]}-1)) - tput cub "$(tput cols)" - else - _current_index=$((_current_index-1)) - - tput cuu1 # Up one line - tput cub "$(tput cols)" # Back to beginning (does this work?) - tput el # Clear to end of line - fi - - if [ "${_checkbox_selected[$_current_index]}" = true ]; then - printf "${cyan}${arrow}${green}${checked}${normal} ${_checkbox_list[$_current_index]} ${normal}" - else - printf "${cyan}${arrow}${normal}${unchecked} ${_checkbox_list[$_current_index]} ${normal}" - fi -} - -on_checkbox_input_up() { - #remove_checkbox_instructions - tput cub "$(tput cols)" - if (( ${_current_row} > 0 )) || (( ${#_checkbox_list[@]} <= 5 )); then - print_checkbox_line $_current_index - tput el - if [ $_current_index = 0 ]; then - _current_index=$((${#_checkbox_list[@]}-1)) - _current_row=4 - tput cud $((${#_checkbox_list[@]}-1)) - tput cub "$(tput cols)" - else - _current_index=$((_current_index-1)) - _current_row=$((_current_row-1)) - tput cuu1 - tput cub "$(tput cols)" - tput el - fi - print_checkbox_line_arrow $_current_index - else - if [ $_current_index = 0 ]; then - _current_index=$((${#_checkbox_list[@]}-1)) - _current_row=4 - tput cuu 1 - tput cub "$(tput cols)" - tput el - printf " ${cyan}${up_arrow}${normal}" - for I in 4 3 2 1; do - tput cud1 - tput cub "$(tput cols)" - tput el - print_checkbox_line $((_current_index-I)) - done - tput cud1 - tput cub "$(tput cols)" - tput el - print_checkbox_line_arrow $((_current_index)) - tput cud1 - tput cub "$(tput cols)" - tput el - printf " ${dim}${down_arrow}${normal}" - tput cuu 1 - else - _current_index=$((_current_index-1)) - tput cud 5 - tput cub "$(tput cols)" - tput el - printf " ${cyan}${down_arrow}${normal}" - for I in 4 3 2 1; do - tput cuu1 - tput cub "$(tput cols)" - tput el - print_checkbox_line $((_current_index+I)) - done - tput cuu1 - tput cub "$(tput cols)" - tput el - print_checkbox_line_arrow $((_current_index)) - if [ $_current_index = 0 ]; then - tput cuu1 - tput cub "$(tput cols)" - tput el - printf " ${dim}${up_arrow}${normal}" - tput cud1 - fi - fi - fi -} - -on_checkbox_input_down() { - #remove_checkbox_instructions - tput cub "$(tput cols)" - if (( ${_current_row} < 4 )) || (( ${#_checkbox_list[@]} <= 5 )); then - print_checkbox_line $_current_index - tput el - if [ $_current_index = $((${#_checkbox_list[@]}-1)) ]; then - _current_index=0 - _current_row=0 - tput cuu $((${#_checkbox_list[@]}-1)) - tput cub "$(tput cols)" - else - _current_index=$((_current_index+1)) - _current_row=$((_current_row+1)) - tput cud1 - tput cub "$(tput cols)" - tput el - fi - print_checkbox_line_arrow $_current_index - else - if [ $_current_index = $((${#_checkbox_list[@]}-1)) ]; then - _current_index=0 - _current_row=0 - tput cuu 5 - tput cub "$(tput cols)" - tput el - printf " ${dim}${up_arrow}${normal}" - tput cud1 - tput cub "$(tput cols)" - tput el - print_checkbox_line_arrow $((_current_index)) - for I in 1 2 3 4; do - tput cud1 - tput cub "$(tput cols)" - tput el - print_checkbox_line $((_current_index+I)) - done - tput cud1 - tput cub "$(tput cols)" - tput el - printf " ${cyan}${down_arrow}${normal}" - tput cuu 5 - else - _current_index=$((_current_index+1)) - tput cuu 5 - tput cub "$(tput cols)" - tput el - printf " ${cyan}${up_arrow}${normal}" - for I in 4 3 2 1; do - tput cud1 - tput cub "$(tput cols)" - tput el - print_checkbox_line $((_current_index-I)) - done - tput cud1 - tput cub "$(tput cols)" - tput el - print_checkbox_line_arrow $((_current_index)) - if [ $_current_index = $((${#_checkbox_list[@]}-1)) ]; then - tput cud1 - tput cub "$(tput cols)" - tput el - printf " ${dim}${down_arrow}${normal}" - tput cuu1 - fi - fi - fi -} - -on_checkbox_input_enter() { - remove_checkbox_instructions - local OLD_IFS - OLD_IFS=$IFS - _checkbox_selected_indices=() - _checkbox_selected_options=() - IFS=$'\n' - - for i in $(gen_index ${#_checkbox_list[@]}); do - if [ "${_checkbox_selected[$i]}" = true ]; then - _checkbox_selected_indices+=($i) - _checkbox_selected_options+=("${_checkbox_list[$i]}") + # Find the length of the longest option to set the dialog width. + for OPTION in "${TRIMMED_OPTIONS[@]}"; do + if [ "${#OPTION}" -gt "$DIALOG_WIDTH" ]; then + DIALOG_WIDTH=${#OPTION} fi done - if (( ${#_checkbox_list[@]} <= 5 )); then - tput cud $((${#_checkbox_list[@]}-${_current_index})) - tput cub "$(tput cols)" - for i in $(seq $((${#_checkbox_list[@]}+1))); do - tput el1 - tput el - tput cuu1 - done - else - tput cud $((6-${_current_row})) - tput cub "$(tput cols)" - for i in $(seq 8); do - tput el1 - tput el - tput cuu1 - done - fi - tput cub "$(tput cols)" + # Apply the offset value to the dialog width. + DIALOG_WIDTH=$((DIALOG_WIDTH + MNU_OPTION_WIDTH_OFFSET)) - tput cuf $((${#prompt}+3)) - printf "${cyan}$(join _checkbox_selected_options)${normal}" - tput el - - tput cud1 - tput cub "$(tput cols)" - tput el - - _break_keypress=true - IFS=$OLD_IFS -} - -on_checkbox_input_space() { - #remove_checkbox_instructions - tput cub "$(tput cols)" - tput el - if [ "${_checkbox_selected[$_current_index]}" = true ]; then - _checkbox_selected[$_current_index]=false - else - _checkbox_selected[$_current_index]=true + # Adjust the dialog width again if the dialog text is longer. + if [ "$DIALOG_WIDTH" -lt $((${#DIALOG_TEXT} + TEXT_WIDTH_OFFSET)) ]; then + DIALOG_WIDTH="$((${#DIALOG_TEXT} + TEXT_WIDTH_OFFSET))" fi - print_checkbox_line_arrow $_current_index -} - -remove_checkbox_instructions() { - if [ $_first_keystroke = true ]; then - tput cuu $((${_current_index}+1)) - tput cub "$(tput cols)" - tput cuf $((${#prompt}+3)) - tput el - tput cud $((${_current_index}+1)) - _first_keystroke=false - fi -} - -# for vim movements -on_checkbox_input_ascii() { - local key=$1 - case $key in - "j" ) on_checkbox_input_down ;; - "k" ) on_checkbox_input_up ;; - esac -} - -_checkbox_input() { - local i - local j - prompt=$1 - eval _checkbox_list=( '"${'${2}'[@]}"' ) - _current_index=0 - _current_row=0 - _first_keystroke=true - - trap control_c SIGINT EXIT - - stty -echo - tput civis - - print "${normal}${green}?${normal} ${bold}${prompt}${normal} ${dim}(Press to select, to finalize)${normal}" - - for i in $(gen_index ${#_checkbox_list[@]}); do - _checkbox_selected[$i]=false + # Pad option text with trailing white space to left-align all options. + for OPTION in "${TRIMMED_OPTIONS[@]}"; do + local PAD_LENGTH=$((DIALOG_WIDTH - MNU_OPTION_WIDTH_OFFSET - ${#OPTION})) + # shellcheck disable=SC2155 + local PADDED_OPTION="${OPTION}$(printf '%*s' $PAD_LENGTH)" + PADDED_OPTIONS+=("$PADDED_OPTION") done - if [ -n "$3" ]; then - eval _selected_indices=( '"${'${3}'[@]}"' ) - for i in ${_selected_indices[@]}; do - _checkbox_selected[$i]=true - done - fi + # Convert options into the appropriate format for a 'dialog' menu. + for PADDED_OPTION in "${PADDED_OPTIONS[@]}"; do + DIALOG_OPTIONS+=("$PADDED_OPTION" "") + done - if (( ${#_checkbox_list[@]} > 5 )); then - tput cub "$(tput cols)" - print " ${dim}${up_arrow}${normal}" - fi + # Store the number of options. + OPTION_NUMBER="${#INPUT_OPTIONS[@]}" - for i in $(gen_index ${#_checkbox_list[@]}); do - tput cub "$(tput cols)" - if [ $i = 0 ]; then - print_checkbox_line_arrow $i - else - print_checkbox_line $i - fi - print "" - tput el - if (( $i > 3 )) && (( ${#_checkbox_list[@]} > 5 )); then - print " ${cyan}${down_arrow}${normal}" - break + # Produce checkbox. + # The output string contains options delimited by spaces. + # Each option is enclosed in double quotes within the output string. + # For example: '"Option 1 " "The Second Option " " Option Number 3 "' + SELECTED_OPTIONS_STRING=$(dialog \ + --keep-tite \ + --clear \ + --no-shadow \ + --menu \ + "$DIALOG_TEXT" \ + "$DIALOG_HEIGHT" \ + "$DIALOG_WIDTH" \ + "$OPTION_NUMBER" \ + "${DIALOG_OPTIONS[@]}" \ + 2>&1 >/dev/tty) || exit 0 + + # Remove white space added previously. + RETURN_STRING=$(echo "$SELECTED_OPTIONS_STRING" | + sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + + # Remove escapes (introduced by 'dialog' if options have parentheses). + RETURN_STRING="${RETURN_STRING//\\/}" # ${variable//search/replace} + + # Display question and response. + echo -e "${ANSI_LIGHT_GREEN}Q) ${ANSI_CLEAR_TEXT}${ANSI_LIGHT_BLUE}${DIALOG_TEXT}${ANSI_CLEAR_TEXT} --> ${ANSI_LIGHT_GREEN}${RETURN_STRING}${ANSI_CLEAR_TEXT}" +} + +function inqChkBx() { + # DECLARE VARIABLES. + # Variables created from function arguments: + declare DIALOG_TEXT="$1" # Dialog heading. + declare INPUT_OPTIONS_VAR="$2" # Input variable name. + declare RETURN_ARRAY_VAR="$3" # Output variable name. + declare -n INPUT_OPTIONS="$INPUT_OPTIONS_VAR" # Input array nameref. + declare -n RETURN_ARRAY="$RETURN_ARRAY_VAR" # Output array nameref. + # Note: namerefs allow changes made through the nameref to affect the + # referenced variable, even across different scopes like function calls. + + # Other variables: + declare TRIMMED_OPTIONS=() # Input array post-trimming. + declare PADDED_OPTIONS=() # Input array with extra white space. + declare DIALOG_OPTIONS=() # Input array for options dialog. + declare DIALOG_WIDTH=0 # Width of dialog window. + declare OPTION_NUMBER=0 # Number of options in dialog window. + declare SELECTED_OPTIONS_STRING="" # Output value from dialog window. + + # MAIN LOGIC. + # Trim leading and trailing white space for each option. + for OPTION in "${INPUT_OPTIONS[@]}"; do + TRIMMED_OPTIONS+=("$(echo "$OPTION" | sed 's/^[ \t]*//;s/[ \t]*$//')") + done + + # Find the length of the longest option to set the dialog width. + for OPTION in "${TRIMMED_OPTIONS[@]}"; do + if [ "${#OPTION}" -gt "$DIALOG_WIDTH" ]; then + DIALOG_WIDTH=${#OPTION} fi done - for j in $(gen_index ${#_checkbox_list[@]}); do - tput cuu1 - if (( $j > 4 )); then - break - fi + # Apply the offset value to the dialog width. + DIALOG_WIDTH=$((DIALOG_WIDTH + CHK_OPTION_WIDTH_OFFSET)) + + # Adjust the dialog width again if the dialog text is longer. + if [ "$DIALOG_WIDTH" -lt $((${#DIALOG_TEXT} + TEXT_WIDTH_OFFSET)) ]; then + DIALOG_WIDTH="$((${#DIALOG_TEXT} + TEXT_WIDTH_OFFSET))" + fi + + # Pad option text with trailing white space to left-align all options. + for OPTION in "${TRIMMED_OPTIONS[@]}"; do + local PAD_LENGTH=$((DIALOG_WIDTH - CHK_OPTION_WIDTH_OFFSET - ${#OPTION})) + # shellcheck disable=SC2155 + local PADDED_OPTION="${OPTION}$(printf '%*s' $PAD_LENGTH)" + PADDED_OPTIONS+=("$PADDED_OPTION") done - on_keypress on_checkbox_input_up on_checkbox_input_down on_checkbox_input_space on_checkbox_input_enter on_default on_default on_checkbox_input_ascii -} - -checkbox_input() { - _checkbox_input "$1" "$2" - _checkbox_input_output_var_name=$3 - select_indices _checkbox_list _checkbox_selected_indices $_checkbox_input_output_var_name - - unset _checkbox_list - unset _break_keypress - unset _first_keystroke - unset _current_index - unset _checkbox_input_output_var_name - unset _checkbox_selected_indices - unset _checkbox_selected_options - - cleanup -} - -checkbox_input_indices() { - _checkbox_input "$1" "$2" "$3" - _checkbox_input_output_var_name=$3 - - eval $_checkbox_input_output_var_name\=\(\) - for i in $(gen_index ${#_checkbox_selected_indices[@]}); do - eval $_checkbox_input_output_var_name\+\=\(${_checkbox_selected_indices[$i]}\) + # Convert options into the appropriate format for a 'dialog' checkbox. + for PADDED_OPTION in "${PADDED_OPTIONS[@]}"; do + DIALOG_OPTIONS+=("$PADDED_OPTION" "" off) done - unset _checkbox_list - unset _break_keypress - unset _first_keystroke - unset _current_index - unset _checkbox_input_output_var_name - unset _checkbox_selected_indices - unset _checkbox_selected_options + # Store the number of options. + OPTION_NUMBER="${#INPUT_OPTIONS[@]}" - cleanup -} + # Produce checkbox. + # The output string contains options delimited by spaces. + # Each option is enclosed in double quotes within the output string. + # For example: '"Option 1 " "The Second Option " " Option Number 3 "' + SELECTED_OPTIONS_STRING=$(dialog \ + --keep-tite \ + --clear \ + --no-shadow \ + --checklist \ + "$DIALOG_TEXT" \ + "$DIALOG_HEIGHT" \ + "$DIALOG_WIDTH" \ + "$OPTION_NUMBER" \ + "${DIALOG_OPTIONS[@]}" \ + 2>&1 >/dev/tty) || exit 0 + # Convert the output string into an array. + # shellcheck disable=SC2001 + while IFS= read -r LINE; do + LINE="${LINE/#\"/}" # Remove leading double quote. + LINE="${LINE/%\"/}" # Remove trailing double quote. + RETURN_ARRAY+=("$LINE") # Add to array. + done < <(echo "$SELECTED_OPTIONS_STRING" | sed 's/\" \"/\"\n\"/g') + # Final modifications. + for ((i = 0; i < ${#RETURN_ARRAY[@]}; i++)); do + # Remove white space added previously. + # shellcheck disable=SC2001 + RETURN_ARRAY[i]=$(echo "${RETURN_ARRAY[i]}" | + sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') - -on_list_input_up() { - remove_list_instructions - tput cub "$(tput cols)" - - printf " ${_list_options[$_list_selected_index]}" - tput el - - if [ $_list_selected_index = 0 ]; then - _list_selected_index=$((${#_list_options[@]}-1)) - tput cud $((${#_list_options[@]}-1)) - tput cub "$(tput cols)" - else - _list_selected_index=$((_list_selected_index-1)) - - tput cuu1 - tput cub "$(tput cols)" - tput el - fi - - printf "${cyan}${arrow} %s ${normal}" "${_list_options[$_list_selected_index]}" -} - -on_list_input_down() { - remove_list_instructions - tput cub "$(tput cols)" - - printf " ${_list_options[$_list_selected_index]}" - tput el - - if [ $_list_selected_index = $((${#_list_options[@]}-1)) ]; then - _list_selected_index=0 - tput cuu $((${#_list_options[@]}-1)) - tput cub "$(tput cols)" - else - _list_selected_index=$((_list_selected_index+1)) - tput cud1 - tput cub "$(tput cols)" - tput el - fi - printf "${cyan}${arrow} %s ${normal}" "${_list_options[$_list_selected_index]}" -} - -on_list_input_enter_space() { - local OLD_IFS - OLD_IFS=$IFS - IFS=$'\n' - - tput cud $((${#_list_options[@]}-${_list_selected_index})) - tput cub "$(tput cols)" - - for i in $(seq $((${#_list_options[@]}+1))); do - tput el1 - tput el - tput cuu1 + # Remove escapes (introduced by 'dialog' if options have parentheses). + RETURN_ARRAY[i]=${RETURN_ARRAY[i]//\\/} # ${variable//search/replace} done - tput cub "$(tput cols)" - - tput cuf $((${#prompt}+3)) - printf "${cyan}${_list_options[$_list_selected_index]}${normal}" - tput el - - tput cud1 - tput cub "$(tput cols)" - tput el - - _break_keypress=true - IFS=$OLD_IFS -} - -remove_list_instructions() { - if [ $_first_keystroke = true ]; then - tput cuu $((${_list_selected_index}+1)) - tput cub "$(tput cols)" - tput cuf $((${#prompt}+3)) - tput el - tput cud $((${_list_selected_index}+1)) - _first_keystroke=false - fi -} - -_list_input() { - local i - local j - prompt=$1 - eval _list_options=( '"${'${2}'[@]}"' ) - - _list_selected_index=0 - _first_keystroke=true - - trap control_c SIGINT EXIT - - stty -echo - tput civis - - print "${normal}${green}?${normal} ${bold}${prompt}${normal} ${dim}(Use arrow keys)${normal}" - - for i in $(gen_index ${#_list_options[@]}); do - tput cub "$(tput cols)" - if [ $i = 0 ]; then - print "${cyan}${arrow} ${_list_options[$i]} ${normal}" - else - print " ${_list_options[$i]}" - fi - tput el - done - - for j in $(gen_index ${#_list_options[@]}); do - tput cuu1 - done - - on_keypress on_list_input_up on_list_input_down on_list_input_enter_space on_list_input_enter_space - -} - - -list_input() { - _list_input "$1" "$2" - local var_name=$3 - eval $var_name=\'"${_list_options[$_list_selected_index]}"\' - unset _list_selected_index - unset _list_options - unset _break_keypress - unset _first_keystroke - - cleanup -} - -list_input_index() { - _list_input "$1" "$2" - local var_name=$3 - eval $var_name=\'"$_list_selected_index"\' - unset _list_selected_index - unset _list_options - unset _break_keypress - unset _first_keystroke - - cleanup -} - - - - -on_text_input_left() { - remove_regex_failed - if [ $_current_pos -gt 0 ]; then - tput cub1 - _current_pos=$(($_current_pos-1)) - fi -} - -on_text_input_right() { - remove_regex_failed - if [ $_current_pos -lt ${#_text_input} ]; then - tput cuf1 - _current_pos=$(($_current_pos+1)) - fi -} - -on_text_input_enter() { - remove_regex_failed - - if [[ "$_text_input" =~ $_text_input_regex && "$(eval $_text_input_validator "$_text_input")" = true ]]; then - tput cub "$(tput cols)" - tput cuf $((${#_read_prompt}-19)) - printf "${cyan}${_text_input}${normal}" - tput el - tput cud1 - tput cub "$(tput cols)" - tput el - eval $var_name=\'"${_text_input}"\' - _break_keypress=true - else - _text_input_regex_failed=true - tput civis - tput cud1 - tput cub "$(tput cols)" - tput el - printf "${red}>>${normal} $_text_input_regex_failed_msg" - tput cuu1 - tput cub "$(tput cols)" - tput cuf $((${#_read_prompt}-19)) - tput el - _text_input="" - _current_pos=0 - tput cnorm - fi -} - -on_text_input_ascii() { - remove_regex_failed - local c=$1 - - if [ "$c" = '' ]; then - c=' ' - fi - - local rest="${_text_input:$_current_pos}" - _text_input="${_text_input:0:$_current_pos}$c$rest" - _current_pos=$(($_current_pos+1)) - - tput civis - printf "$c$rest" - tput el - if [ ${#rest} -gt 0 ]; then - tput cub ${#rest} - fi - tput cnorm -} - -on_text_input_backspace() { - remove_regex_failed - if [ $_current_pos -gt 0 ]; then - local start="${_text_input:0:$(($_current_pos-1))}" - local rest="${_text_input:$_current_pos}" - _current_pos=$(($_current_pos-1)) - tput cub 1 - tput el - tput sc - printf "$rest" - tput rc - _text_input="$start$rest" - fi -} - -remove_regex_failed() { - if [ $_text_input_regex_failed = true ]; then - _text_input_regex_failed=false - tput sc - tput cud1 - tput el1 - tput el - tput rc - fi -} - -text_input_default_validator() { - echo true; -} - -text_input() { - local prompt=$1 - local var_name=$2 - local _text_input_regex="${3:-"\.+"}" - local _text_input_regex_failed_msg=${4:-"Input validation failed"} - local _text_input_validator=${5:-text_input_default_validator} - local _read_prompt_start=$'\e[32m?\e[39m\e[1m' - local _read_prompt_end=$'\e[22m' - local _read_prompt="$( echo "$_read_prompt_start ${prompt} $_read_prompt_end")" - local _current_pos=0 - local _text_input_regex_failed=false - local _text_input="" - printf "$_read_prompt" - - - trap control_c SIGINT EXIT - - stty -echo - tput cnorm - - on_keypress on_default on_default on_text_input_ascii on_text_input_enter on_text_input_left on_text_input_right on_text_input_ascii on_text_input_backspace - eval $var_name=\'"${_text_input}"\' - - cleanup -} - -# ============================================================= - -function menuFromCmd() { - local mLOCALRESULT=$1 - local mRESULT='' - read -r -a ARRAY <<< $3 - list_input "$2" ARRAY mRESULT - eval $mLOCALRESULT="'$mRESULT'" -} - -function menuFromArr() { - local mLOCALRESULT=$1 - shift - local PROMPT=$1 - shift - local ARRAY=("$@") - list_input "$PROMPT" ARRAY mRESULT - eval $mLOCALRESULT="'$mRESULT'" -} - -function multiFromArr() { - local mLOCALRESULT=$1 - shift - local PROMPT=$1 - shift - local ARRAY=("$@") - checkbox_input "$PROMPT" ARRAY mRESULT - eval $mLOCALRESULT="'$mRESULT'" } diff --git a/installer.sh b/installer.sh index a2f109d..9a3d4c4 100755 --- a/installer.sh +++ b/installer.sh @@ -1,425 +1,1421 @@ #!/usr/bin/env bash +# shellcheck disable=SC2034 # Silence warnings regarding unused variables globally. -if ! command -v bc &> /dev/null -then - echo "You need bc!" - exit -fi +### 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 -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +# 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 privilages 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 VM powered off. +readonly EC_VM_PAUSED="9" # Windows VM paused. +readonly EC_VM_ABSENT="10" # Windows VM does not exist. +readonly EC_VM_NO_IP="11" # Windows VM does not have an IP address. +readonly EC_VM_BAD_PORT="12" # Windows VM is unreachable via RDP_PORT. +readonly EC_RDP_FAIL="13" # FreeRDP failed to establish a connection with the Windows VM. +readonly EC_APPQUERY_FAIL="14" # Failed to query the Windows VM for installed applications. -MAKEDEMO=0 -USEDEMO=0 +# 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. +# '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 oath 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 the Windows VM for applications. +readonly BATCH_SCRIPT_PATH_WIN="${USER_APPDATA_PATH_WIN}\\installed.bat" # WINDOWS path to a batch script used to search the Windows VM 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 VM. +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 VM. +# '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. -# shellcheck disable=SC1094 -. "$DIR/install/inquirer.sh" +# REMOTE DESKTOP CONFIGURATION +readonly VM_NAME="RDPWindows" # Name of the Windows VM. +readonly RDP_PORT=3389 # Port used for RDP on the Windows VM. +readonly WINAPPS_CONFIG='RDP_USER="MyWindowsUser" +RDP_PASS="MyWindowsPassword" +#RDP_DOMAIN="MYDOMAIN" +#RDP_IP="192.168.123.111" +#RDP_SCALE=100 +#RDP_FLAGS="" +#MULTIMON="true" +#DEBUG="true" +#FREERDP_COMMAND="xfreerdp"' # Default WinApps configuration file content. -INSTALLED_EXES=() +### 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. +RDP_SCALE=100 # Imported variable. +RDP_FLAGS="" # Imported variable. +MULTIMON="false" # Imported variable. +DEBUG="false" # Imported variable. +FREERDP_COMMAND="" # Imported variable. +MULTI_FLAG="" # Set based on value of $MULTIMON. + +# 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'. + +# 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 "Usage: - ./installer.sh --user # Install everything in $HOME - ./installer.sh --system # Install everything in /usr" - exit + echo -e "Usage: + ${COMMAND_TEXT}./installer.sh --user${CLEAR_TEXT} # Install WinApps and selected applications in ${HOME} + ${COMMAND_TEXT}./installer.sh --system${CLEAR_TEXT} # Install WinApps and selected applications in /usr + ${COMMAND_TEXT}./installer.sh --user --setupAllOfficiallySupportedApps${CLEAR_TEXT} # Install WinApps and all officially supported applications in ${HOME} + ${COMMAND_TEXT}./installer.sh --system --setupAllOfficiallySupportedApps${CLEAR_TEXT} # Install WinApps and all officially supported applications in /usr + ${COMMAND_TEXT}./installer.sh --user --uninstall${CLEAR_TEXT} # Uninstall everything in ${HOME} + ${COMMAND_TEXT}./installer.sh --system --uninstall${CLEAR_TEXT} # Uninstall everything in /usr + ${COMMAND_TEXT}./installer.sh --help${CLEAR_TEXT} # Display this usage message." } -function waNoSudo() { - echo "You are attempting to switch from a --system install to a --user install. - Please run \"./installer.sh --system --uninstall\" first." - exit +# Name: 'waSetWorkingDirectory' +# Role: Changes the working directory to the directory containing the script. +function waSetWorkingDirectory() { + # 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]}")") + + # Silently change the working directory. + if ! cd "$SCRIPT_DIR_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}${SCRIPT_DIR_PATH}${CLEAR_TEXT}${INFO_TEXT}.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Ensure:" + echo -e " - ${COMMAND_TEXT}${SCRIPT_DIR_PATH}${CLEAR_TEXT} exists." + echo -e " - ${COMMAND_TEXT}${SCRIPT_DIR_PATH}${CLEAR_TEXT} is valid and does not contain syntax errors." + echo -e " - The current user has sufficient permissions to access ${COMMAND_TEXT}${SCRIPT_DIR_PATH}${CLEAR_TEXT}." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_FAILED_CD" + fi } -function waInstall() { - $SUDO mkdir -p "$SYS_PATH/apps" - . "$DIR/bin/winapps" install -} +# 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. -function waFindInstalled() { - echo -n " Checking for installed apps in RDP machine (this may take a while)..." - if [ $USEDEMO != 1 ]; then - rm -f "$HOME/.local/share/winapps/installed.bat" - rm -f "$HOME/.local/share/winapps/installed.tmp" - rm -f "$HOME/.local/share/winapps/installed" - rm -f "$HOME/.local/share/winapps/detected" - cp "$DIR/install/ExtractPrograms.ps1" "$HOME/.local/share/winapps/ExtractPrograms.ps1" - for F in "$DIR"/apps/*; do - [[ -e "$F" ]] || break - F="$(basename "$F")" + 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}" - # shellcheck disable=SC1090,SC1091 - . "$DIR/apps/$F/info" - printf "IF EXIST \"%s\" ECHO %s >> %s\n" "$WIN_EXECUTABLE" "$F" '\\tsclient\home\.local\share\winapps\installed.tmp' >> "$HOME/.local/share/winapps/installed.bat" + # 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 - printf "%s\n" 'powershell.exe -ExecutionPolicy Bypass -File \\tsclient\home\.local\share\\winapps\ExtractPrograms.ps1 > \\tsclient\home\.local\share\winapps\detected' >> "$HOME/.local/share/winapps/installed.bat" - printf "%s\n" 'RENAME \\tsclient\home\.local\share\winapps\installed.tmp installed' >> "$HOME/.local/share/winapps/installed.bat" - # shellcheck disable=SC2140 - $FREERDP_COMMAND /d:"$RDP_DOMAIN" /u:"$RDP_USER" /p:"$RDP_PASS" +auto-reconnect +home-drive -wallpaper +span /app:program:"C:\Windows\System32\cmd.exe",cmd:"$(printf '/C %s' '\\tsclient\home\.local\share\winapps\installed.bat')" /v:"$RDP_IP" 1>/dev/null 2>&1 & - COUNT=0 - while [ ! -f "$HOME/.local/share/winapps/installed" ]; do - sleep 5 - COUNT=$((COUNT + 1)) - if ((COUNT == 15)); then - echo " Finished." - echo "" - echo "The RDP connection failed to connect or run. Please confirm FreeRDP can connect with:" - echo " bin/winapps check" - echo "" - echo "If it cannot connect, this is most likely due to:" - echo " - You need to accept the security cert the first time you connect (with 'check')" - echo " - Not enabling RDP in the Windows VM" - echo " - Not being able to connect to the IP of the VM" - echo " - Incorrect user credentials in winapps.conf" - echo " - Not merging install/RDPApps.reg into the VM" - exit - fi - done - if [ $MAKEDEMO = 1 ]; then - rm -rf /tmp/winapps_demo - cp -a "$HOME/.local/share/winapps" /tmp/winapps_demo - exit - fi else - rm -rf "$HOME/.local/share/winapps" - cp -a /tmp/winapps_demo "$HOME/.local/share/winapps" - #sleep 3 + # 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 - echo " Finished." } -function waConfigureApp() { - if [[ -z $1 ]]; then - return 1 +# Name: 'waConfigurePathsAndPermissions' +# Role: Sets paths and adjusts permissions as specified. +function waConfigurePathsAndPermissions() { + if [ "$OPT_USER" -eq 1 ]; then + SUDO="" + BIN_PATH="$USER_BIN_PATH" + APP_PATH="$USER_APP_PATH" + APPDATA_PATH="$USER_APPDATA_PATH" + elif [ "$OPT_SYSTEM" -eq 1 ]; then + SUDO="sudo" + 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 - if [ -z "$ICON" ]; then - ICON=$SYS_PATH/apps/$1/icon.$2 +} + +# 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" ]; 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}./installer.sh --user --uninstall${CLEAR_TEXT}." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_EXISTING_INSTALL" fi - # shellcheck disable=SC1090 - . "$SYS_PATH/apps/$1/info" - echo -n " Configuring $NAME..." - if [ $USEDEMO != 1 ]; then - $SUDO rm -f "$APP_PATH/$1.desktop" - echo "[Desktop Entry] -Name=$NAME -Exec=$BIN_PATH/winapps $1 %F + + # Check for an existing 'system' installation. + if [ -f "${SYS_BIN_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}./installer.sh --system --uninstall${CLEAR_TEXT}." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_EXISTING_INSTALL" + fi + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" +} + +# 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 "\nThe configuration file should contain the following:" + echo -e "\n${COMMAND_TEXT}${WINAPPS_CONFIG}${CLEAR_TEXT}" + echo -e "\nThe ${COMMAND_TEXT}RDP_USER${CLEAR_TEXT} and ${COMMAND_TEXT}RDP_PASS${CLEAR_TEXT} fields should contain the Windows user's account name and password." + echo -e "Note that the Windows user's PIN combination CANNOT be used to populate ${COMMAND_TEXT}RDP_PASS${CLEAR_TEXT}." + 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: 'waCheckDependencies' +# Role: Terminate script if dependencies are missing. +function waCheckDependencies() { + # Declare variables. + local FREERDP_MAJOR_VERSION="" # Stores the major version of the installed copy of FreeRDP. + + # Print feedback. + echo -n "Checking whether all dependencies are installed... " + + # 'Dialog'. + if ! command -v dialog &>/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 '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 + + # '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*' | cut -d'.' -f1) + if [[ $FREERDP_MAJOR_VERSION =~ ^[0-9]+$ ]] && ((FREERDP_MAJOR_VERSION >= 3)); then + FREERDP_COMMAND="xfreerdp" + fi + elif 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*' | cut -d'.' -f1) + if [[ $FREERDP_MAJOR_VERSION =~ ^[0-9]+$ ]] && ((FREERDP_MAJOR_VERSION >= 3)); then + FREERDP_COMMAND="xfreerdp3" + 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'. + 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 + + # 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 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_STATE=$(virsh list --all | grep -w "$VM_NAME") + + if [[ $VM_STATE == *"shut off"* ]]; 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_STATE == *"paused"* ]]; 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_STATE != *"running"* ]]; 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: 'waCheckVMContactable' +# Role: Assesses whether the Windows VM can be contacted (prior to attempting a remote desktop connection). +function waCheckVMContactable() { + # Print feedback. + echo -n "Attempting to contact the Windows VM... " + + # Declare variables. + local VM_MAC="" # Stores the MAC address of the Windows VM. + + # Obtain Windows VM IP Address + if [ -z "$RDP_IP" ]; 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=$(arp -n | 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_VM_NO_IP" + fi + fi + + # Check for an open RDP port. + if ! timeout 5 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 the Windows VM '${VM_NAME}' at '${RDP_IP}:${RDP_PORT}'.${CLEAR_TEXT}" + + # Display the suggested action(s). + echo "--------------------------------------------------------------------------------" + echo "Please ensure Remote Desktop is configured on the Windows VM as per the WinApps README." + echo "--------------------------------------------------------------------------------" + + # Terminate the script. + return "$EC_VM_BAD_PORT" + fi + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" +} + +# Name: 'waCheckRDPAccess' +# Role: Tests if the Windows VM is accessible via remote desktop. +function waCheckRDPAccess() { + # Print feedback. + echo -n "Establishing a Remote Desktop connection with the Windows VM... " + + # 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 the Windows VM 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 \ + +home-drive \ + -wallpaper \ + +dynamic-resolution \ + /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 30 seconds for the background process to complete. + while [ "$ELAPSED_TIME" -lt 30 ]; 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" + 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 the Windows VM '${VM_NAME}'.${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 the Windows VM prior to initiating the WinApps installation." + echo " - Ensure the credentials within the WinApps configuration file are correct." + echo " - Ensure the Windows VM is correctly named as specified within the README." + echo " - Ensure 'Remote Desktop' is enabled within the Windows VM." + echo " - Ensure you have merged 'RDPApps.reg' into the Windows VM's registry." + echo -e " - Utilise a new certificate by removing relevant certificate(s) in ${COMMAND_TEXT}${HOME}/.config/freerdp/server${CLEAR_TEXT}." + 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 the Windows VM. +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 the Windows VM. + 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")" + + # 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 it's 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 the Windows VM 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 \ + +home-drive \ + -wallpaper \ + +dynamic-resolution \ + /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 60 seconds for the batch script to finish running. + while [ $ELAPSED_TIME -lt 60 ]; 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" + 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 VM '${VM_NAME}' 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 "--------------------------------------------------------------------------------" + + # 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 VM via Remote Desktop. +function waConfigureWindows() { + # Print feedback. + echo -n "Creating an application entry for Windows VM... " + + # Declare variables. + local WIN_BASH="" # Stores the bash script to launch the Windows VM. + local WIN_DESKTOP="" # Stores the '.desktop' file to launch the Windows VM. + + # 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=$ICON -StartupWMClass=$FULL_NAME -Comment=$FULL_NAME -Categories=$CATEGORIES -MimeType=$MIME_TYPES - " | $SUDO tee "$APP_PATH/$1.desktop" >/dev/null - $SUDO rm -f "$BIN_PATH/$1" - echo -e "#!/usr/bin/env bash \n $BIN_PATH/winapps $1 $* - " | $SUDO tee "$BIN_PATH/$1" >/dev/null - $SUDO chmod a+x "$BIN_PATH/$1" - fi - echo " Finished." +Icon=${APPDATA_PATH}/icons/windows.svg +StartupWMClass=Microsoft Windows +Comment=Microsoft Windows VM" - ICON="" + # 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}" } -function waConfigureApps() { - APPS=() - while IFS= read -r F; do - [[ -n $F ]] || continue - F=$(echo "$F" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') - # shellcheck disable=SC1090 - . "$DIR/apps/$F/info" - APPS+=("$FULL_NAME ($F)") - INSTALLED_EXES+=("$(echo "${WIN_EXECUTABLE##*\\}" | tr '[:upper:]' '[:lower:]')") - done < <(sed 's/\r/\n/g' < "$HOME/.local/share/winapps/installed") - IFS=$'\n' - # FIXME - # shellcheck disable=SC2207 - APPS=($(sort <<<"${APPS[*]}")) - unset IFS - OPTIONS=("Set up all detected pre-configured applications" "Select which pre-configured applications to set up" "Do not set up any pre-configured applications") +# Name: 'waConfigureApp' +# Role: Create application entries for a given application installed on the Windows VM. +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. - if [ "$INSTALL_TYPE" != 'User' ]; then - menuFromArr APP_INSTALL "How would you like to handle WinApps pre-configured applications?" "${OPTIONS[@]}" - else "grep -l -d skip" - menuFromArr APP_INSTALL "How would you like to handle WinApps pre-configured applications? If any web browser is set-up, may be configured as default browser." "${OPTIONS[@]}" - fi - if [ "$APP_INSTALL" = "Select which pre-configured applications to set up" ]; then - checkbox_input "Which pre-configured apps would you like to set up?" APPS SELECTED_APPS - echo "" >"$HOME/.local/share/winapps/installed" - for F in "${SELECTED_APPS[@]}"; do - APP="${F##*(}" - APP="${APP%%)}" - echo "${APP}" >>"$HOME/.local/share/winapps/installed" + # Source 'Info' File Containing: + # - The Application Name (FULL_NAME) + # - The Shortcut Nsame (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 the Windows VM. +function waConfigureOfficiallySupported() { + # Declare variables. + local OSA_LIST=() # Stores a list of all officially supported applications installed on the Windows VM. + + # Read the list of officially supported applications that are installed on the Windows VM 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) + + # 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 + + # 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 the Windows VM. + 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 the Windows VM 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 the Windows VM. + for OSA in "${OSA_LIST[@]}"; do + # Source 'Info' File Containing: + # - The Application Name (FULL_NAME) + # - The Shortcut Nsame (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 - $SUDO cp "$DIR/bin/winapps" "$BIN_PATH/winapps" - COUNT=0 - if [ "$APP_INSTALL" != "Do not set up any pre-configured applications" ]; then - while IFS= read -r F; do - F=$(echo "$F" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') - COUNT=$((COUNT + 1)) - $SUDO cp -r "apps/$F" "$SYS_PATH/apps" - waConfigureApp "$F" svg - done < <(sed 's/\r/\n/g' < "$HOME/.local/share/winapps/installed") - fi - rm -f "$HOME/.local/share/winapps/installed" - rm -f "$HOME/.local/share/winapps/installed.bat" - if ((COUNT == 0)); then - echo " No configured applications." - fi -} - -function waConfigureAppsAllOfficiallySupported(){ - $SUDO cp "$DIR/bin/winapps" "$BIN_PATH/winapps" - COUNT=0 - while IFS= read -r F; do - F=$(echo "$F" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') - - COUNT=$((COUNT + 1)) - $SUDO cp -r "apps/$F" "$SYS_PATH/apps" - waConfigureApp "$F" svg - done < <(sed 's/\r/\n/g' < "$HOME/.local/share/winapps/installed") - rm -f "$HOME/.local/share/winapps/installed" - rm -f "$HOME/.local/share/winapps/installed.bat" - if ((COUNT == 0)); then - echo " No configured applications." + # 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() { - if [ -f "$HOME/.local/share/winapps/detected" ]; then - sed -i 's/\r//g' "$HOME/.local/share/winapps/detected" - # shellcheck disable=SC1091 - . "$HOME/.local/share/winapps/detected" - APPS=() - # shellcheck disable=SC2153 - for I in "${!NAMES[@]}"; do - EXE=${EXES[$I]##*\\} - EXE_LOWER=$(echo "$EXE" | tr '[:upper:]' '[:lower:]') - if ( - dlm=$'\x1F' - IFS="$dlm" - [[ "$dlm${INSTALLED_EXES[*]}$dlm" != *"$dlm$EXE_LOWER$dlm"* ]] - ); then - APPS+=("${NAMES[$I]} ($EXE)") + # 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 the Windows VM. + 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 - IFS=$'\n' APPS=("$(sort <<<"${APPS[*]}")") + + # 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 - OPTIONS=("Set up all detected applications" "Select which applications to set up" "Do not set up any applications") - menuFromArr APP_INSTALL "How would you like to handle other detected applications?" "${OPTIONS[@]}" - if [ "$APP_INSTALL" = "Select which applications to set up" ]; then - checkbox_input "Which other apps would you like to set up?" APPS SELECTED_APPS - echo "" >"$HOME/.local/share/winapps/installed" - for F in "${SELECTED_APPS[@]}"; do - EXE="${F##*(}" - EXE="${EXE%%)}" - APP="${F% (*}" - echo "$EXE|${APP}" >>"$HOME/.local/share/winapps/installed" - done - elif [ "$APP_INSTALL" = "Set up all detected applications" ]; then - for I in "${!EXES[@]}"; do - EXE=${EXES[$I]##*\\} - echo "$EXE|${NAMES[$I]}" >>"$HOME/.local/share/winapps/installed" - done + 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 + readarray -t SELECTED_APPS <<<"${APPS[@]}" fi - COUNT=0 - if [ -f "$HOME/.local/share/winapps/installed" ]; then - while read -r LINE; do - EXE="${LINE%|*}" - NAME="${LINE#*|}" - for I in "${!NAMES[@]}"; do - if [ "$NAME" = "${NAMES[$I]}" ] && [[ "${EXES[$I]}" == *"\\$EXE" ]]; then - EXE=$(echo "$EXE" | tr '[:upper:]' '[:lower:]') - $SUDO mkdir -p "$SYS_PATH/apps/$EXE" - echo "# GNOME shortcut name -NAME=\"$NAME\" -# Used for descriptions and window class -FULL_NAME=\"$NAME\" + 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%%)}" -# The executable inside windows -WIN_EXECUTABLE=\"${EXES[$I]}\" + # Capture the substring prior to the space and parentheses. + # This substring represents the detected application name (see above loop). + PROGRAM_NAME="${SELECTED_APP% (*}" -# GNOME categories + # 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}\" +# Executable within Windows VM +WIN_EXECUTABLE=\"${EXES[$INDEX]}\" +# GNOME Categories CATEGORIES=\"WinApps\" +# GNOME MIME Types +MIME_TYPES=\"\"" -# GNOME mimetypes -MIME_TYPES=\"\" - " | sudo tee "$SYS_PATH/apps/$EXE/info" >/dev/null - # shellcheck disable=SC2153 - echo "${ICONS[$I]}" | base64 -d | sudo tee "$SYS_PATH/apps/$EXE/icon.ico" >/dev/null - waConfigureApp "$EXE" ico - COUNT=$((COUNT + 1)) - fi - done - done <"$HOME/.local/share/winapps/installed" - rm -f "$HOME/.local/share/winapps/installed" - fi - rm -f "$HOME/.local/share/winapps/installed.bat" - if ((COUNT == 0)); then - echo " No configured applications." - fi + # 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.ico" &>/dev/null + + # Configure the application. + waConfigureApp "$EXE_FILENAME_NOEXT" ico + + # Print feedback. + echo -e "${DONE_TEXT}Done!${CLEAR_TEXT}" + fi + done + done fi } -function waConfigureWindows() { - echo -n " Configuring Windows..." - if [ $USEDEMO != 1 ]; then - $SUDO rm -f "$APP_PATH/windows.desktop" - $SUDO mkdir -p "$SYS_PATH/icons" - $SUDO cp "$DIR/icons/windows.svg" "$SYS_PATH/icons/windows.svg" - echo "[Desktop Entry] -Name=Windows -Exec=$BIN_PATH/winapps windows %F -Terminal=false -Type=Application -Icon=$SYS_PATH/icons/windows.svg -StartupWMClass=Microsoft Windows -Comment=Microsoft Windows - " | $SUDO tee "$APP_PATH/windows.desktop" >/dev/null - $SUDO rm -f "$BIN_PATH/windows" - echo "#!/usr/bin/env bash -$BIN_PATH/winapps windows - " | $SUDO tee "/$BIN_PATH/windows" >/dev/null - $SUDO chmod a+x "$BIN_PATH/windows" +# 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. + waCheckDependencies + + # Update $MULTI_FLAG. + if [[ $MULTIMON == "true" ]]; then + MULTI_FLAG="/multimon" + else + MULTI_FLAG="+span" fi - echo " Finished." + + # Append additional FreeRDP flags if required. + if [[ -n $RDP_FLAGS ]]; then + FREERDP_COMMAND="${FREERDP_COMMAND} ${RDP_FLAGS}" + fi + + # Check the group membership of the current user. + waCheckGroupMembership + + # Check if the Windows VM is powered on. + waCheckVMRunning + + # Check if the Windows VM is contactable. + waCheckVMContactable + + # Test RDP access to the Windows VM. + 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 script. + $SUDO cp "./bin/winapps" "${BIN_PATH}/winapps" + + # Configure the Windows VM 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 + + # Print feedback. + echo -e "${SUCCESS_TEXT}INSTALLATION COMPLETE.${CLEAR_TEXT}" } -function waUninstallUser() { - rm -f "$HOME/.local/bin/winapps" - rm -rf "$HOME/.local/share/winapps" - grep -l -d skip "bin/winapps" "$HOME/.local/share/applications/"* -s | while IFS= read -r F - do - F=$(echo "$F" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') +# 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}" - echo -n " Removing $F..." - $SUDO rm "$F" - echo " Finished." - done - grep -l -d skip "bin/winapps" "$HOME/.local/bin/"* -s | while IFS= read -r F - do - F=$(echo "$F" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + # 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. - echo -n " Removing $F..." - $SUDO rm "$F" - echo " Finished." + # Remove the 'WinApps' bash script. + $SUDO rm -f "${BIN_PATH}/winapps" + + # 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 your WinApps configuration file was not removed.${CLEAR_TEXT}" + echo -e "${INFO_TEXT}You can remove this manually by running:${CLEAR_TEXT}" + echo -e "${COMMAND_TEXT}rm ${CONFIG_PATH}${CLEAR_TEXT}\n" + + # Print feedback. + echo -e "${SUCCESS_TEXT}UNINSTALLATION COMPLETE.${CLEAR_TEXT}" } -function waUninstallSystem() { - $SUDO rm -f "/usr/local/bin/winapps" - $SUDO rm -rf "/usr/local/share/winapps" - grep -l -d skip "bin/winapps" "/usr/share/applications/"* -s | while IFS= read -r F - do - F=$(echo "$F" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') +### SEQUENTIAL LOGIC ### +# Welcome the user. +echo -e "${BOLD_TEXT}\ +################################################################################ +# # +# WinApps Install Wizard # +# # +################################################################################ +${CLEAR_TEXT}" - if [ -z "$SUDO" ]; then - waNoSudo - fi - echo -n " Removing $F..." - $SUDO rm "$F" - echo " Finished." - done - grep -l -d skip "bin/winapps" "/usr/local/bin/"* -s | while IFS= read -r F - do - F=$(echo "$F" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') +# Source the contents of 'inquirer.sh'. +# shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck. +source "$INQUIRER_PATH" - if [ -z "$SUDO" ]; then - waNoSudo - fi - echo -n " Removing $F..." - $SUDO rm "$F" - echo " Finished." - done -} +# Set the working directory. +waSetWorkingDirectory -if [ -z "$1" ]; then - OPTIONS=(User System) - menuFromArr INSTALL_TYPE "Would you like to install for the current user or the whole system?" "${OPTIONS[@]}" -elif [ "$1" = '--user' ]; then - INSTALL_TYPE='User' -elif [ "$1" = '--system' ]; then - INSTALL_TYPE='System' +# Sanitise and parse the user input. +waCheckInput "$@" + +# Configure paths and permissions. +waConfigurePathsAndPermissions + +# Install or uninstall WinApps. +if [ "$OPT_UNINSTALL" -eq 1 ]; then + waUninstall else - waUsage + waInstall fi -if [ "$INSTALL_TYPE" = 'User' ]; then - SUDO="" - BIN_PATH="$HOME/.local/bin" - APP_PATH="$HOME/.local/share/applications" - SYS_PATH="$HOME/.local/share/winapps" - mkdir -p "$BIN_PATH" - mkdir -p "$APP_PATH" - mkdir -p "$SYS_PATH" - if [ -n "$2" ]; then - if [ "$2" = '--uninstall' ]; then - # Uninstall - echo "Uninstalling..." - waUninstallUser - exit - elif [ "$2" = '--setupAllOfficiallySupportedApps' ]; then - echo "Setting up All Officially Supported Apps " - echo "Removing any old configurations..." - waUninstallUser - waUninstallSystem - waInstall - waFindInstalled - waConfigureWindows - waConfigureAppsAllOfficiallySupported - exit - else - usage - fi - fi -elif [ "$INSTALL_TYPE" = 'System' ]; then - SUDO="sudo" - sudo ls >/dev/null - BIN_PATH="/usr/local/bin" - APP_PATH="/usr/share/applications" - SYS_PATH="/usr/local/share/winapps" - if [ -n "$2" ]; then - if [ "$2" = '--uninstall' ]; then - # Uninstall - echo "Uninstalling..." - waUninstallSystem - exit - elif [ "$2" = '--setupAllOfficiallySupportedApps' ]; then - echo "Setting up All Officially Supported Apps " - echo "Removing any old configurations..." - waUninstallUser - waUninstallSystem - - echo "Installing..." - waInstall - waFindInstalled - waConfigureWindows - waConfigureAppsAllOfficiallySupported - exit - else - usage - fi - fi -fi - -echo "Removing any old configurations..." -waUninstallUser -waUninstallSystem - -echo "Installing..." - -# Inititialize -waInstall - -# Check for installed apps -waFindInstalled - -# Install windows -waConfigureWindows - -# Configure apps -waConfigureApps -waConfigureDetectedApps - -echo "Installation complete." +exit 0