Merge pull request #139 from KernelGhost/improve-install-script

[Code Refactor] Improved Installation Script
This commit is contained in:
LDprg 2024-07-16 18:48:43 +02:00 committed by GitHub
commit b811acea64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 1972 additions and 1529 deletions

View File

@ -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

284
README.md
View File

@ -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.
<img src="demo/demo.gif" width=1000 alt="WinApps Demonstration Animation.">
<img src="demo/demo.gif" width=1000>
## 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 <u>*ALL*</u> 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
<table cellpadding="10" cellspacing="0" border="0">
<tr>
<td><img src="apps/acrobat-x-pro/icon.svg" width="100"></td><td>Adobe Acrobat Pro<br>(X)</td>
<td><img src="apps/acrobat-reader-dc/icon.svg" width="100"></td><td>Adobe Acrobat Reader<br>(DC)</td>
</tr>
<tr>
<td><img src="apps/aftereffects-cc/icon.svg" width="100"></td><td>Adobe After Effects<br>(CC)</td>
<td><img src="apps/audition-cc/icon.svg" width="100"></td><td>Adobe Audition<br>(CC)</td>
</tr>
<tr>
<td><img src="apps/bridge-cs6/icon.svg" width="100"></td><td>Adobe Bridge<br>(CS6, CC)</td>
<td><img src="apps/adobe-cc/icon.svg" width="100"></td><td>Adobe Creative Cloud<br>(CC)</td>
</tr>
<tr>
<td><img src="apps/illustrator-cc/icon.svg" width="100"></td><td>Adobe Illustrator<br>(CC)</td>
<td><img src="apps/indesign-cc/icon.svg" width="100"></td><td>Adobe InDesign<br>(CC)</td>
</tr>
<tr>
<td><img src="apps/lightroom-cc/icon.svg" width="100"></td><td>Adobe Lightroom<br>(CC)</td>
<td><img src="apps/cmd/icon.svg" width="100"></td><td>Command Prompt<br>(cmd.exe)</td>
</tr>
<tr>
<td><img src="apps/explorer/icon.svg" width="100"></td><td>Explorer<br>(File Manager)</td>
<td><img src="apps/iexplorer/icon.svg" width="100"></td><td>Internet Explorer<br>(11)</td>
</tr>
<tr>
<td><img src="apps/access/icon.svg" width="100"></td><td>Microsoft Access<br>(2016, 2019, o365)</td>
<td><img src="apps/excel/icon.svg" width="100"></td><td>Microsoft Excel<br>(2016, 2019, o365)</td>
</tr>
<tr>
<td><img src="apps/word/icon.svg" width="100"></td><td>Microsoft Word<br>(2016, 2019, o365)</td>
<td><img src="apps/onenote/icon.svg" width="100"></td><td>Microsoft OneNote<br>(2016, 2019, o365)</td>
</tr>
<tr>
<td><img src="apps/outlook/icon.svg" width="100"></td><td>Microsoft Outlook<br>(2016, 2019, o365)</td>
<td><img src="apps/powerpoint/icon.svg" width="100"></td><td>Microsoft PowerPoint<br>(2016, 2019, o365)</td>
</tr>
<tr>
<td><img src="apps/publisher/icon.svg" width="100"></td><td>Microsoft Publisher<br>(2016, 2019, o365)</td>
<td><img src="apps/powershell/icon.svg" width="100"></td><td>PowerShell</td>
</tr>
<tr>
<td><img src="icons/windows.svg" width="100"></td><td>Windows<br>(Full RDP session)</td>
<td>&nbsp;</td><td>&nbsp;</td>
</tr>
<tr>
<td><img src="apps/acrobat-x-pro/icon.svg" width="100"></td><td>Adobe Acrobat Pro<br>(X)</td>
<td><img src="apps/aftereffects-cc/icon.svg" width="100"></td><td>Adobe After Effects<br>(CC)</td>
</tr>
<tr>
<td><img src="apps/audition-cc/icon.svg" width="100"></td><td>Adobe Audition<br>(CC)</td>
<td><img src="apps/bridge-cs6/icon.svg" width="100"></td><td>Adobe Bridge<br>(CS6, CC)</td>
</tr>
<tr>
<td><img src="apps/adobe-cc/icon.svg" width="100"></td><td>Adobe Creative Cloud<br>(CC)</td>
<td><img src="apps/illustrator-cc/icon.svg" width="100"></td><td>Adobe Illustrator<br>(CC)</td>
</tr>
<tr>
<td><img src="apps/indesign-cc/icon.svg" width="100"></td><td>Adobe InDesign<br>(CC)</td>
<td><img src="apps/lightroom-cc/icon.svg" width="100"></td><td>Adobe Lightroom<br>(CC)</td>
</tr>
<tr>
<td><img src="apps/cmd/icon.svg" width="100"></td><td>Command Prompt<br>(cmd.exe)</td>
<td><img src="apps/explorer/icon.svg" width="100"></td><td>Explorer<br>(File Manager)</td>
</tr>
<tr>
<td><img src="apps/iexplorer/icon.svg" width="100"></td><td>Internet Explorer<br>(11)</td>
<td><img src="apps/access/icon.svg" width="100"></td><td>Microsoft Access<br>(2016, 2019, o365)</td>
</tr>
<tr>
<td><img src="apps/excel/icon.svg" width="100"></td><td>Microsoft Excel<br>(2016, 2019, o365)</td>
<td><img src="apps/word/icon.svg" width="100"></td><td>Microsoft Word<br>(2016, 2019, o365)</td>
</tr>
<tr>
<td><img src="apps/onenote/icon.svg" width="100"></td><td>Microsoft OneNote<br>(2016, 2019, o365)</td>
<td><img src="apps/outlook/icon.svg" width="100"></td><td>Microsoft Outlook<br>(2016, 2019, o365)</td>
</tr>
<tr>
<td><img src="apps/powerpoint/icon.svg" width="100"></td><td>Microsoft PowerPoint<br>(2016, 2019, o365)</td>
<td><img src="apps/publisher/icon.svg" width="100"></td><td>Microsoft Publisher<br>(2016, 2019, o365)</td>
</tr>
<tr>
<td><img src="apps/powershell/icon.svg" width="100"></td><td>PowerShell</td>
<td><img src="icons/windows.svg" width="100"></td><td>Windows<br>(Full RDP session)</td>
</tr>
</table>
## 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`.
<img src="demo/installer.gif" width=1000>
## 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)

View File

@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 4.4 MiB

View File

@ -1,3 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048">
<path d="M0 268l768-107v735H0V268zM1920 0v896H896V143L1920 0zM896 1024h1024v896L896 1777v-753zm-896 0h768v735L0 1652v-628z" fill="#73A1FB"/>
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="2048" height="2048">
<title>POO</title>
<defs>
<linearGradient id="g1" x2="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-2048,-2048,2048,-2048,2048,2048)">
<stop offset="0" stop-color="#067cd6"/>
<stop offset=".4" stop-color="#0f85da"/>
<stop offset="1" stop-color="#7adcff"/>
</linearGradient>
</defs>
<style>
.s0 { fill: url(#g1) }
</style>
<path id="Windows" fill-rule="evenodd" class="s0" d="m228 0h746v974h-974v-746c0-125.9 102.1-228 228-228zm746 2048h-746c-125.9 0-228-102.1-228-228v-746h974zm846-2048c125.9 0 228 102.1 228 228v746h-974v-974zm228 1820c0 125.9-102.1 228-228 228h-746v-974h974z"/>
</svg>

Before

Width:  |  Height:  |  Size: 217 B

After

Width:  |  Height:  |  Size: 751 B

View File

@ -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 <space> to select, <enter> 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'"
}

File diff suppressed because it is too large Load Diff