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: remove-crlf
- id: forbid-tabs - id: forbid-tabs
- id: remove-tabs - id: remove-tabs
args: [ --whitespaces-count, "2" ] args: [ --whitespaces-count, "4" ]
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0 rev: v4.6.0
@ -43,7 +43,8 @@ repos:
- repo: https://github.com/scop/pre-commit-shfmt - repo: https://github.com/scop/pre-commit-shfmt
rev: v3.8.0-1 rev: v3.8.0-1
hooks: hooks:
- id: shfmt - id: shfmt
args: ["-i", "4", "-ci", "-s"]
- repo: https://github.com/shellcheck-py/shellcheck-py - repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1 rev: v0.10.0.1

284
README.md
View File

@ -1,114 +1,121 @@
# WinApps # 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, <img src="demo/demo.gif" width=1000 alt="WinApps Demonstration Animation.">
including Nautilus integration for right-clicking on files of specific mime types to open them.
<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 Universal application support is achieved by:
- Checking the RDP server for installed applications such as Microsoft Office 1. Scanning Windows for any officially supported applications (list below).
- If those programs are installed, it creates shortcuts leveraging FreeRDP for both the CLI and the GNOME tray 2. Scanning Windows for any other `.exe` files listed within the Windows Registry.
- 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
## 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: *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.*
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.
### Officially Supported Applications
<table cellpadding="10" cellspacing="0" border="0"> <table cellpadding="10" cellspacing="0" border="0">
<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/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> <td><img src="apps/aftereffects-cc/icon.svg" width="100"></td><td>Adobe After Effects<br>(CC)</td>
</tr> </tr>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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> </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>
</table> </table>
## Installation ## 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. 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.
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:
- [Creating a virtual machine with Docker](docs/docker.md) ### Step 2: Clone WinApps Repository and Dependencies
- [Creating a virtual machine in KVM (outdated)](docs/KVM.md) 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 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).
To get things going, use:
```bash ```bash
sudo apt install -y freerdp3-x11 flatpak install flathub com.freerdp.FreeRDP
git clone https://github.com/winapps-org/winapps.git sudo flatpak override --filesystem=home com.freerdp.FreeRDP # To use `+home-drive`
cd winapps
``` ```
> [!note] ### Step 3: Create a WinApps Configuration File
> Requires FreeRDP 3.0.0 or later. Create a configuration file at `~/.config/winapps/winapps.conf` containing the following:
> 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:
```bash ```bash
RDP_USER="MyWindowsUser" RDP_USER="MyWindowsUser"
RDP_PASS="MyWindowsPassword" RDP_PASS="MyWindowsPassword"
@ -121,95 +128,52 @@ RDP_PASS="MyWindowsPassword"
#FREERDP_COMMAND="xfreerdp" #FREERDP_COMMAND="xfreerdp"
``` ```
The username and password should be a full user account and password, such as the one created when setting up Windows `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.
or a domain user. It can't be a user/PIN combination as those aren't valid for RDP access.
Options: #### 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.
- When using a pre-existing non-KVM RDP server, you must use the `RDP_IP` to specify its location - 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.
- 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`.
- 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].
- 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 microphone, uncomment and use the `RDP_FLAGS` configuration option.
- 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`. A FreeRDP bug may result in a black screen however, in which case you should revert this change.
- For multi-monitor setups, you can try enabling `MULTIMON`, however, if you get a black screen (FreeRDP bug) you will need to revert
- If you enable `DEBUG`, a log will be created on each application start in `~/.local/share/winapps/winapps.log` - 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. - 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
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:
### Step 4: Run the WinApps Installer
Run the WinApps installer.
```bash ```bash
./installer.sh ./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> <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: ## 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.
- 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.
```bash ```bash
./bin/winapps manual "C:\my\directory\executableNotInPath.exe" ./bin/winapps manual "C:\my\directory\executableNotInPath.exe"
./bin/winapps manual executableInPath.exe ./bin/winapps manual executableInPath.exe
``` ```
## Checking for new application support ## Updating WinApps
The installer can be run multiple times. To update your installation of WinApps:
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. 1. Run the WinApps installer to remove WinApps from your system.
2. Pull the latest changes from the WinApps GitHub repository.
```bash 3. Re-install WinApps using the WinApps installer.
./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
```
## Shout-outs ## Shout-outs
Some icons used for the officially supported applications were sourced from:
- Some icons pulled from - Fluent UI React - Icons under [MIT License](https://github.com/Fmstrat/fluent-ui-react/blob/master/LICENSE.md)
- 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)
- 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)
- 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)
- 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 #!/usr/bin/env bash
if [ ! -f "$HOME/.config/winapps/winapps.conf" ] && [ ! -f "$HOME/.winapps" ]; then ### GLOBAL CONSTANTS ###
echo "You need to create a ~/.config/winapps/winapps.conf configuration. Exiting..." # ANSI ESCAPE SEQUENCES
exit readonly ERROR_TEXT="\033[1;41;37m" # Bold + White + Red Background
fi readonly CLEAR_TEXT="\033[0m" # Clear
DIR="$(dirname "$(readlink -f "$0")")" # ERROR CODES
RUN="$(date)-$RANDOM" 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 # PATHS
mkdir -p "$HOME/.local/share/winapps" readonly APPDATA_PATH="${HOME}/.local/share/winapps"
fi 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 RDP_SCALE=100
MULTIMON="false"
DEBUG="true"
MULTI_FLAG=""
if [ -f "$HOME/.config/winapps/winapps.conf" ]; then ### FUNCTIONS ###
# shellcheck source=/dev/null # Name: 'waThrowExit'
. "$HOME/.config/winapps/winapps.conf" # Role: Throw an error message and exit the script.
else function waThrowExit() {
# shellcheck source=/dev/null # Declare variables.
. "$HOME/.winapps" local ERR_CODE="$1"
fi
# 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() { function dprint() {
if [ "$DEBUG" = "true" ]; then [ "$DEBUG" = "true" ] && echo "[$RUN] $1" >>"$LOG_PATH"
echo "[$RUN] $1" >>"$HOME/.local/share/winapps/winapps.log" }
# 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 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" dprint "START"
dprint "SCRIPT_DIR: ${SCRIPT_DIR_PATH}"
if [ -f "$HOME/.local/share/winapps/run" ]; then dprint "SCRIPT_ARGS: ${*}"
LAST_RAN=$(stat -t -c %Y "$HOME/.local/share/winapps/run") dprint "HOME_DIR: ${HOME}"
dprint "LAST_RAN:${LAST_RAN}" mkdir -p "$APPDATA_PATH"
touch "$HOME/.local/share/winapps/run" waLastRun
THIS_RUN=$(stat -t -c %Y "$HOME/.local/share/winapps/run") waLoadConfig
dprint "THIS_RUN:$THIS_RUN" waGetFreeRDPCommand
if ((THIS_RUN - LAST_RAN < 2)); then waCheckGroupMembership
exit waCheckVMRunning
fi waCheckVMContactable
else waRunCommand "$@"
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 "END" 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"> <svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="2048" height="2048">
<path d="M0 268l768-107v735H0V268zM1920 0v896H896V143L1920 0zM896 1024h1024v896L896 1777v-753zm-896 0h768v735L0 1652v-628z" fill="#73A1FB"/> <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> </svg>

Before

Width:  |  Height:  |  Size: 217 B

After

Width:  |  Height:  |  Size: 751 B

View File

@ -1,835 +1,186 @@
#!/usr/bin/env bash #!/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 # MAIN LOGIC.
# Trim leading and trailing white space for each option.
# Permission is hereby granted, free of charge, to any person obtaining a copy for OPTION in "${INPUT_OPTIONS[@]}"; do
# of this software and associated documentation files (the "Software"), to deal TRIMMED_OPTIONS+=("$(echo "$OPTION" | sed 's/^[ \t]*//;s/[ \t]*$//')")
# 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
done done
for i in ${_indices[@]}; do # Find the length of the longest option to set the dialog width.
_checkbox_selected[$i]=true for OPTION in "${TRIMMED_OPTIONS[@]}"; do
done if [ "${#OPTION}" -gt "$DIALOG_WIDTH" ]; then
DIALOG_WIDTH=${#OPTION}
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]}")
fi fi
done done
if (( ${#_checkbox_list[@]} <= 5 )); then # Apply the offset value to the dialog width.
tput cud $((${#_checkbox_list[@]}-${_current_index})) DIALOG_WIDTH=$((DIALOG_WIDTH + MNU_OPTION_WIDTH_OFFSET))
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)"
tput cuf $((${#prompt}+3)) # Adjust the dialog width again if the dialog text is longer.
printf "${cyan}$(join _checkbox_selected_options)${normal}" if [ "$DIALOG_WIDTH" -lt $((${#DIALOG_TEXT} + TEXT_WIDTH_OFFSET)) ]; then
tput el DIALOG_WIDTH="$((${#DIALOG_TEXT} + TEXT_WIDTH_OFFSET))"
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
fi fi
print_checkbox_line_arrow $_current_index # 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}))
remove_checkbox_instructions() { # shellcheck disable=SC2155
if [ $_first_keystroke = true ]; then local PADDED_OPTION="${OPTION}$(printf '%*s' $PAD_LENGTH)"
tput cuu $((${_current_index}+1)) PADDED_OPTIONS+=("$PADDED_OPTION")
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
done done
if [ -n "$3" ]; then # Convert options into the appropriate format for a 'dialog' menu.
eval _selected_indices=( '"${'${3}'[@]}"' ) for PADDED_OPTION in "${PADDED_OPTIONS[@]}"; do
for i in ${_selected_indices[@]}; do DIALOG_OPTIONS+=("$PADDED_OPTION" "")
_checkbox_selected[$i]=true done
done
fi
if (( ${#_checkbox_list[@]} > 5 )); then # Store the number of options.
tput cub "$(tput cols)" OPTION_NUMBER="${#INPUT_OPTIONS[@]}"
print " ${dim}${up_arrow}${normal}"
fi
for i in $(gen_index ${#_checkbox_list[@]}); do # Produce checkbox.
tput cub "$(tput cols)" # The output string contains options delimited by spaces.
if [ $i = 0 ]; then # Each option is enclosed in double quotes within the output string.
print_checkbox_line_arrow $i # For example: '"Option 1 " "The Second Option " " Option Number 3 "'
else SELECTED_OPTIONS_STRING=$(dialog \
print_checkbox_line $i --keep-tite \
fi --clear \
print "" --no-shadow \
tput el --menu \
if (( $i > 3 )) && (( ${#_checkbox_list[@]} > 5 )); then "$DIALOG_TEXT" \
print " ${cyan}${down_arrow}${normal}" "$DIALOG_HEIGHT" \
break "$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 fi
done done
for j in $(gen_index ${#_checkbox_list[@]}); do # Apply the offset value to the dialog width.
tput cuu1 DIALOG_WIDTH=$((DIALOG_WIDTH + CHK_OPTION_WIDTH_OFFSET))
if (( $j > 4 )); then
break # Adjust the dialog width again if the dialog text is longer.
fi 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 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 # Convert options into the appropriate format for a 'dialog' checkbox.
} for PADDED_OPTION in "${PADDED_OPTIONS[@]}"; do
DIALOG_OPTIONS+=("$PADDED_OPTION" "" off)
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]}\)
done done
unset _checkbox_list # Store the number of options.
unset _break_keypress OPTION_NUMBER="${#INPUT_OPTIONS[@]}"
unset _first_keystroke
unset _current_index
unset _checkbox_input_output_var_name
unset _checkbox_selected_indices
unset _checkbox_selected_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:]]*$//')
# Remove escapes (introduced by 'dialog' if options have parentheses).
on_list_input_up() { RETURN_ARRAY[i]=${RETURN_ARRAY[i]//\\/} # ${variable//search/replace}
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
done 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