diff --git a/.includes/dialog-interface.sh b/.includes/dialog-interface.sh index afda207..dd2e80a 100644 --- a/.includes/dialog-interface.sh +++ b/.includes/dialog-interface.sh @@ -83,6 +83,82 @@ agm_menu() { fi } +# Specialized game launcher menu with "Read Documentation" button +# Usage: agm_game_menu "title" "backtitle" "text" option1 "description1" option2 "description2" ... +# Returns: exit code 0=Launch, 1=Cancel, 2/3=Documentation (yad/dialog) +agm_game_menu() { + local title="$1" + local backTitle="$2" + local text="$3" + shift 3 + + if [[ "$dialogType" == "yad" ]]; then + # Use custom buttons to match dialog behavior (launch/doc/cancel) + local yadList="" + declare -A valueMap + while [[ $# -gt 0 ]]; do + local option="$1" + local description="$2" + valueMap["$description"]="$option" + if [[ -n "$yadList" ]]; then + yadList="$yadList\n" + fi + yadList="${yadList}${description}" + shift 2 + done + + local selectedDescription + selectedDescription=$(echo -e "$yadList" | yad --list \ + --title="$title" \ + --text="$text" \ + --column="Option" \ + --no-headers \ + --selectable-labels \ + --search-column=1 \ + --height=400 \ + --width=600 \ + --button="Launch:0" \ + --button="Read Documentation:2" \ + --button="Cancel:1") + local menuCode=$? + + # Strip trailing pipes and return the mapped value + if [[ -n "$selectedDescription" ]]; then + selectedDescription="${selectedDescription%|}" + echo "${valueMap["$selectedDescription"]}" + fi + return $menuCode + else + # Build dialog menu format with mapping (same approach as yad) + local dialogArgs=() + declare -A valueMap + while [[ $# -gt 0 ]]; do + local option="$1" + local description="$2" + valueMap["$description"]="$option" + dialogArgs+=("$description" "$description") + shift 2 + done + + local selectedDescription + selectedDescription=$(dialog --backtitle "$backTitle" \ + --title "$title" \ + --extra-button \ + --extra-label "Read Documentation" \ + --no-tags \ + --menu "$text" 0 0 0 \ + "${dialogArgs[@]}" \ + --stdout) + local menuCode=$? + + # Return the mapped value + if [[ -n "$selectedDescription" ]]; then + echo "${valueMap["$selectedDescription"]}" + fi + return $menuCode + fi +} + # Wrapper function for checklist selection # Usage: agm_checklist "title" "backtitle" "text" option1 "description1" "status1" option2 "description2" "status2" ... agm_checklist() { @@ -181,24 +257,41 @@ agm_msgbox() { } # Wrapper function for yes/no dialog -# Usage: agm_yesno "title" "backtitle" "text" +# Usage: agm_yesno "title" "backtitle" "text" ["yes_label"] ["no_label"] agm_yesno() { local title="$1" local backTitle="$2" local text="$3" - + local yesLabel="${4:-Yes}" + local noLabel="${5:-No}" + if [[ "$dialogType" == "yad" ]]; then echo -e "$text" | yad --text-info \ --title="$title" \ --show-cursor \ - --button="Yes:0" \ - --button="No:1" \ + --button="$yesLabel:0" \ + --button="$noLabel:1" \ --width=600 \ --height=400 else - dialog --backtitle "$backTitle" \ - --title "$title" \ - --yesno "$text" 0 0 + # dialog --yesno doesn't support custom labels, use menu instead + if [[ "$yesLabel" != "Yes" ]] || [[ "$noLabel" != "No" ]]; then + # Custom labels requested, use menu + local choice + choice=$(dialog --backtitle "$backTitle" \ + --title "$title" \ + --no-tags \ + --menu "$text" 0 0 0 \ + "1" "$yesLabel" \ + "2" "$noLabel" \ + --stdout) + [[ "$choice" == "1" ]] + else + # Default Yes/No + dialog --backtitle "$backTitle" \ + --title "$title" \ + --yesno "$text" 0 0 + fi fi } @@ -441,4 +534,4 @@ agm_dselect() { --dselect "$defaultPath" 0 0 \ --stdout fi -} \ No newline at end of file +} diff --git a/.includes/help.sh b/.includes/help.sh index 3ee8097..a8b6fd4 100644 --- a/.includes/help.sh +++ b/.includes/help.sh @@ -5,16 +5,21 @@ documentation() { if [[ "$2" == "Donate" ]]; then return fi - if ! command -v w3m &> /dev/null ; then - echo "This feature of audiogame-manager requires w3m. Please install it before continuing." - exit 1 - fi - get_bottle "$1" -echo "Loading documentation, please wait..." + + # Extract architecture from first parameter (format: "win64|path") + local wineArch="${1%%|*}" + get_bottle "$wineArch" + + echo "Loading documentation, please wait..." + # Try to find documentation based on common naming conventions. - local gamePath="$(winepath -u "$2" 2> /dev/null)" + local gamePath + gamePath="$(winepath -u "$2" 2> /dev/null)" gamePath="${gamePath%/*}" - local gameDoc="$(find "$gamePath" -type f -iname 'user_manual.htm*' -or -iname 'user manual.htm*' -or -iname '*user guide.htm*' | head -1)" + local gameDoc="" + local isUrl="false" + + gameDoc="$(find "$gamePath" -type f -iname 'user_manual.htm*' -or -iname 'user manual.htm*' -or -iname '*user guide.htm*' | head -1)" # Game name specific docs, add the name to the for loop. if [[ -z "$gameDoc" ]]; then for i in "troopanum.txt" "superdeekout.txt" scw.html ; do @@ -46,12 +51,47 @@ echo "Loading documentation, please wait..." gameDoc="$(find "$gamePath" -type f -iname '*.url' -exec grep -i 'url=' {} \; | grep -iv 'score' | head -1)" gameDoc="${gameDoc#*=}" gameDoc="${gameDoc//[[:cntrl:]]/}" + [[ -n "$gameDoc" ]] && isUrl="true" fi - # Display documentation if available. + + # Display documentation if available if [[ -n "$gameDoc" ]]; then - w3m "$gameDoc" + if [[ "$isUrl" == "true" ]]; then + # URL extracted from .url file - open in browser + open_url "$gameDoc" + elif [[ "$dialogType" == "yad" ]]; then + # GUI mode: use appropriate viewer + if [[ "${gameDoc,,}" =~ \.(html?)$ ]]; then + # HTML files: use xdg-open for default browser + xdg-open "$gameDoc" 2>/dev/null & + else + # Text files: use yad text-info for accessibility + yad --text-info \ + --title="Game Documentation" \ + --filename="$gameDoc" \ + --width=800 \ + --height=600 \ + --button="Close:0" + fi + else + # Console mode: use w3m or fallback + if command -v w3m &> /dev/null; then + w3m "$gameDoc" + elif [[ "${gameDoc,,}" =~ \.(html?)$ ]]; then + echo "Install w3m to view HTML documentation in console mode." + echo "Documentation location: $gameDoc" + read -rp "Press Enter to continue..." + else + less "$gameDoc" + fi + fi else - echo "No documentation found." + if [[ "$dialogType" == "yad" ]]; then + agm_msgbox "Documentation" "" "No documentation found for this game." + else + echo "No documentation found." + read -rp "Press Enter to continue..." + fi fi exit 0 } diff --git a/audiogame-manager.sh b/audiogame-manager.sh index fc428a5..6aa5838 100755 --- a/audiogame-manager.sh +++ b/audiogame-manager.sh @@ -2,6 +2,8 @@ # Get script directory for relative paths scriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Default to running normally unless explicitly set by a caller. +agmNoLaunch="${agmNoLaunch:-false}" # Dialog accessibility export DIALOGOPTS='--no-lines --visit-items' @@ -123,7 +125,7 @@ EOF fi echo "# Wine64 bottle creation complete." - } | agm_progressbox "Wine Bottle Setup" "Creating unified wine64 bottle with SAPI support (this may take several minutes)..." + } > >(agm_progressbox "Wine Bottle Setup" "Creating unified wine64 bottle with SAPI support (this may take several minutes)...") fi } @@ -164,8 +166,10 @@ game_installer() { menuList+=("Donate" "Donate") menuList+=("Become a Patron" "Become a Patron") # Show game selection dialog - game="$(agm_menu "Audio Game Installer" "Audio Game Installer" "Please select a game to install" "${menuList[@]}")" - [[ $? -ne 0 ]] && exit 0 + local game + if ! game="$(agm_menu "Audio Game Installer" "Audio Game Installer" "Please select a game to install" "${menuList[@]}")"; then + exit 0 + fi # Handle selection if [[ -n "$game" ]]; then case "$game" in @@ -183,6 +187,7 @@ game_installer() { # Check if install script exists if [[ -f "$installScript" ]]; then # Source and execute the install script + # shellcheck disable=SC1090 source "$installScript" # Show success message agm_msgbox "Installation Complete" "Audio Game Installer" "Game \"${game}\" has been successfully installed." @@ -197,6 +202,7 @@ game_installer() { # remove games game_removal() { + # shellcheck source=.includes/bottle.sh source "${scriptDir}/.includes/bottle.sh" # Modern wine is unified - no architecture-specific wine executables needed mapfile -t lines < <(sed -e '/^$/d' -e '/^[[:space:]]*#/d' "${configFile}" 2> /dev/null) @@ -235,6 +241,7 @@ game_removal() { create_game_array "$selectedGame" if [[ ${#game[@]} -gt 0 ]]; then # Set up wine environment for this game + # shellcheck source=.includes/bottle.sh source "${scriptDir}/.includes/bottle.sh" get_bottle "${game[0]}" @@ -248,7 +255,8 @@ game_removal() { # Remove only the game's installation directory if [[ -n "$winePath" ]]; then - local gameDir="$(winepath "$winePath")" + local gameDir + gameDir="$(winepath "$winePath")" if [[ -d "$gameDir" ]]; then rm -rfv "$gameDir" | agm_progressbox "Removing Game" "Removing \"${game[2]}\" files..." else @@ -265,6 +273,7 @@ game_removal() { # kill games that are stuck kill_game() { + # shellcheck source=.includes/bottle.sh source "${scriptDir}/.includes/bottle.sh" # Modern wine is unified - no architecture-specific wine executables needed mapfile -t lines < <(sed '/^$/d' "${configFile}" 2> /dev/null) @@ -280,7 +289,8 @@ kill_game() { done menuList+=("Donate" "Donate") menuList+=("Become a Patron" "Become a Patron") - local game="$(agm_menu "Audio Game Killer" "Audio Game Killer" "Please select a game to force stop" "${menuList[@]}")" + local game + game="$(agm_menu "Audio Game Killer" "Audio Game Killer" "Please select a game to force stop" "${menuList[@]}")" if [[ ${#game} -gt 0 ]]; then if [[ "$game" == "Donate" ]]; then open_url "https://ko-fi.com/stormux" @@ -307,9 +317,9 @@ kill_game() { custom_launch_parameters() { if [[ "${game[2]}" == "Dragon Pong" ]]; then "${scriptDir}/speech/speak_window_title.sh" DragonPong.exe & - pushd "$(winepath "$winePath")" + pushd "$(winepath "$winePath")" || exit 1 wine "$wineExec" - popd + popd || exit 1 exit 0 fi if [[ "${game[2]}" == "Dreamland" ]]; then @@ -373,21 +383,21 @@ custom_launch_parameters() { exit 0 fi if [[ "${game[2]}" == "Screaming Strike 2" ]]; then - pushd "$(winepath "$winePath")" + pushd "$(winepath "$winePath")" || exit 1 wine "$wineExec" - popd + popd || exit 1 exit 0 fi if [[ "${game[2]}" == "Warsim" ]]; then - pushd "$(winepath "${game[1]%\\*}")" + pushd "$(winepath "${game[1]%\\*}")" || exit 1 wine "${game[1]##*\\}" - popd + popd || exit 1 exit 0 fi if [[ "${game[2]}" == "Interceptor" ]]; then - pushd "$(winepath "$winePath")" + pushd "$(winepath "$winePath")" || exit 1 wine "$wineExec" - popd + popd || exit 1 exit 0 fi } @@ -404,11 +414,11 @@ process_launcher_flags() { create_game_array() { # Game array 0 bottle, 1 path, 2 title, 3+ flags - game="$1" + local selectedGame="$1" for i in "${lines[@]}" ; do # Only compare the launcher section - j="${game#*|}" - k="${i#*|}" + local j="${selectedGame#*|}" + local k="${i#*|}" k="${k%%|*}" if [[ "$j" == "$k" ]]; then IFS='|' read -ra game <<< "$i" @@ -420,6 +430,7 @@ create_game_array() { # Update NVDA controller client DLLs in wine bottles update_nvda_dlls() { # Ensure we have the replacement DLLs + # shellcheck source=.includes/functions.sh source "${scriptDir}/.includes/functions.sh" download "${nvdaControllerClient32Dll}" "${nvdaControllerClient64Dll}" @@ -461,12 +472,13 @@ update_nvda_dlls() { game_launcher() { # For use by update scripts that want to source functions in this file. [[ "$agmNoLaunch" == "true" ]] && return + # shellcheck source=.includes/bottle.sh source "${scriptDir}/.includes/bottle.sh" # Start nvda2speechd if available if ! ss -ltnp | rg 3457 | grep -q 'cthulhu'; then - if [[ -x ${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/nvda2speechd ]]; then - ${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/nvda2speechd &> /dev/null & + if [[ -x "${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/nvda2speechd" ]]; then + "${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/nvda2speechd" &> /dev/null & fi fi @@ -486,25 +498,40 @@ game_launcher() { done menuList+=("Donate" "Donate") menuList+=("Become a Patron" "Become a Patron") - local game="" - game="$(agm_menu "Audio Game Launcher" "Audio Game Launcher" "Please select a game to play" "${menuList[@]}")" - local menuCode=$? - if [[ $menuCode -eq 1 ]] || [[ $menuCode -eq 255 ]]; then - exit 0 - elif [[ $menuCode -eq 3 ]]; then - source "${scriptDir}/.includes/help.sh" # Make available in this function - documentation "$game" "$(echo "$game" | cut -d '|' -f2)" - fi - + + # Loop to handle documentation views + local selectedGame="" + local menuCode=0 + while true; do + selectedGame="$(agm_game_menu "Audio Game Launcher" "Audio Game Launcher" "Please select a game to play" "${menuList[@]}")" + menuCode=$? + + if [[ $menuCode -eq 1 ]] || [[ $menuCode -eq 255 ]]; then + exit 0 + elif [[ $menuCode -eq 2 ]] || [[ $menuCode -eq 3 ]]; then + # Documentation button pressed + if [[ -n "$selectedGame" ]]; then + # shellcheck source=.includes/help.sh + source "${scriptDir}/.includes/help.sh" + documentation "$selectedGame" "$(echo "$selectedGame" | cut -d '|' -f2)" + fi + # Return to menu after viewing docs + continue + else + # dialog mode with OK (0) - proceed with launch + break + fi + done + # Safety check: don't proceed if game is empty - if [[ -z "$game" ]]; then + if [[ -z "$selectedGame" ]]; then exit 0 fi - - create_game_array "$game" + + create_game_array "$selectedGame" else - create_game_array "$game" - if [[ -z "$game" ]]; then + create_game_array "$1" + if [[ ${#game[@]} -eq 0 ]]; then agm_msgbox "Audio Game Launcher" "" "Game $1 not found." exit 1 fi @@ -544,9 +571,9 @@ game_launcher() { custom_launch_parameters if [[ "$debugGdb" == "1" ]]; then # Change to game directory before launching - pushd "$(winepath "${game[1]%\\*}")" > /dev/null + pushd "$(winepath "${game[1]%\\*}")" > /dev/null || exit 1 winedbg --gdb "${game[1]##*\\}" - popd > /dev/null + popd > /dev/null || exit 1 else wine start /d "${game[1]%\\*}" "${game[1]##*\\}" /realtime fi @@ -570,6 +597,7 @@ else fi # Source dialog interface early for progress display +# shellcheck source=.includes/dialog-interface.sh source "${scriptDir}/.includes/dialog-interface.sh" # If display isn't set assume we are launching from console and an X environment is running using display :0 @@ -587,6 +615,7 @@ winetricksPath="${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager" mkdir -p "${winetricksPath}" # Load any arguments from settings.conf file if [[ -r "${configFile%/*}/settings.conf" ]]; then + # shellcheck disable=SC1091 source "${configFile%/*}/settings.conf" fi # Update the cache for older versions of audiogame-manager @@ -594,6 +623,7 @@ if [[ -d "${configFile%/*}/cache" ]]; then { mv -v "${configFile%/*}/cache/"* "${cache}" rmdir -v "${configFile%/*}/cache/"; } | agm_progressbox "Audiogame Manager" "Updating cache, please wait..." fi +# shellcheck disable=SC2034 checkWinetricksUpdate="false" # Turn off debug messages export WINEDEBUG="${winedebug:--all}" @@ -612,14 +642,20 @@ export ipfsGateway="${ipfsGateway:-https://ipfs.stormux.org}" # Source helper functions +# shellcheck source=.includes/bottle.sh source "${scriptDir}/.includes/bottle.sh" # Also sourced in functions that need it +# shellcheck source=.includes/desktop.sh source "${scriptDir}/.includes/desktop.sh" # dialog-interface.sh already sourced earlier +# shellcheck source=.includes/functions.sh source "${scriptDir}/.includes/functions.sh" +# shellcheck source=.includes/help.sh source "${scriptDir}/.includes/help.sh" +# shellcheck source=.includes/update.sh source "${scriptDir}/.includes/update.sh" # Set NVDA controller client DLLs from centralized ipfs array +# shellcheck disable=SC2154 export nvdaControllerClient32Dll="${ipfs[nvdaControllerClient32]}" export nvdaControllerClient64Dll="${ipfs[nvdaControllerClient64]}" export nvda2speechdBinary="${ipfs[nvda2speechd]}" @@ -683,28 +719,46 @@ while getopts "${args}" i ; do h) help;; i) game_installer;; I) - export game="${OPTARG}" + export selectedGameName="${OPTARG}" export noninteractiveInstall="true" break;; k) kill_game;; L) license;; l) game_launcher "${OPTARG}";; - N) noCache="true";; - n) norh="true";; + N) + # shellcheck disable=SC2034 + noCache="true" + ;; + n) + # shellcheck disable=SC2034 + norh="true" + ;; P) checklist quiet;; q) noqjoypad="true" game_launcher;; - R) redownload="true";; + R) + # shellcheck disable=SC2034 + redownload="true" + ;; r) game_removal;; - S) defaultRate="${OPTARG}";; + S) + # shellcheck disable=SC2034 + defaultRate="${OPTARG}" + ;; t) gameCount=$(find "${scriptDir}/.install" -type f -iname "*.sh" | wc -l) agm_infobox "Linux Game Manager" "Linux Game Manager" "There are currently ${gameCount} games available." exit 0 ;; - v) voiceName="${OPTARG}";; - V) defaultVoice="${OPTARG}";; + v) + # shellcheck disable=SC2034 + voiceName="${OPTARG}" + ;; + V) + # shellcheck disable=SC2034 + defaultVoice="${OPTARG}" + ;; esac done @@ -714,16 +768,17 @@ done # Only proceed with noninteractive installation if explicitly requested if [[ "$noninteractiveInstall" == "true" ]]; then # If no game specified for noninteractive install, exit - [[ ${#game} -lt 1 ]] && exit 0 + [[ -z "$selectedGameName" ]] && exit 0 # Install the specified game noninteractively - if [[ -f "${scriptDir}/.install/${game}.sh" ]]; then + if [[ -f "${scriptDir}/.install/${selectedGameName}.sh" ]]; then export LANG="en_US.UTF-8" - . "${scriptDir}/.install/${game}.sh" + # shellcheck disable=SC1090 + . "${scriptDir}/.install/${selectedGameName}.sh" # Show success message - agm_msgbox "Installation Complete" "Audio Game Installer" "Game \"${game}\" has been successfully installed." + agm_msgbox "Installation Complete" "Audio Game Installer" "Game \"${selectedGameName}\" has been successfully installed." else - agm_msgbox "Audio Game Installer" "" "Error: Game '${game}' not found in .install directory" + agm_msgbox "Audio Game Installer" "" "Error: Game '${selectedGameName}' not found in .install directory" exit 1 fi fi