diff --git a/README.md b/README.md index f962da6..ea9285c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,6 @@ An uppercase I looks like a 1, 3 from i3, and 8 because the song [We Are 138](ht - i3-wm: The i3 window manager. - jq: for getting the current workspace - libnotify: For sending notifications -- lxsession: [optional] For GUI power options like shutdown - magic-wormhole: [optional] for file sharing with magic-wormhole GUI - notification-daemon: To handle notifications - pamixer: for the mute-unmute script diff --git a/i38.sh b/i38.sh index 87c4af1..c21e31a 100755 --- a/i38.sh +++ b/i38.sh @@ -169,7 +169,7 @@ menulist() { rangebox() { dialog --title "I38" \ --backtitle "Use the arrow keys to select a number, then press enter." \ - --rangebox "$1" -1 -1 $2 $3 $4 --stdout + --rangebox "$1" -1 -1 "$2" "$3" "$4" --stdout } yesno() { @@ -179,161 +179,85 @@ yesno() { return $? } -# Custom application keybinding functions -declare -A usedKeys -declare -a customApps +# Personal mode helpers +normalize_ratpoison_key() { + local key="$1" -showKeybindingHelp() { - dialog --title "I38 Keybinding Help" --msgbox \ -"Keybinding Notation: -Modifiers: ^ = Ctrl, ! = Alt, # = Super, m = your mod key -Special keys: f1-f12, up/down/left/right, space, tab, return - home/end/insert/delete/pageup/pagedown, print - backspace, escape + key="${key//Alt/Mod1}" + key="${key//Super Left/Super_L}" + key="${key//Super Right/Super_R}" -Examples: - c = just 'c' key - ^c = Ctrl+c - !f1 = Alt+F1 - mspace = mod+Space - ^!up = Ctrl+Alt+Up - #pagedown = Super+Page_Down - -Uppercase letters imply Shift (e.g., C = Shift+c)" 0 0 + echo "$key" } -convertKeybinding() { - local input="$1" - local result="" - local baseKey="" - - # Handle modifiers - if [[ "$input" == *"^"* ]]; then - result+="Control+" - input="${input//^/}" - fi - if [[ "$input" == *"!"* ]]; then - result+="Mod1+" - input="${input//!/}" - fi - if [[ "$input" == *"#"* ]]; then - result+="Mod4+" - input="${input//#/}" - fi - if [[ "$input" == *"m"* ]]; then - result+="\$mod+" - input="${input//m/}" - fi - - # Handle shift for uppercase letters - if [[ "$input" =~ [A-Z] ]]; then - result+="Shift+" - input="${input,,}" - fi - - # Convert special keys - case "$input" in - f[1-9]|f1[0-2]) baseKey="F${input#f}" ;; - up|down|left|right) baseKey="$input" ;; - space) baseKey="space" ;; - tab) baseKey="Tab" ;; - return) baseKey="Return" ;; - escape) baseKey="Escape" ;; - backspace) baseKey="BackSpace" ;; - print) baseKey="Print" ;; - home) baseKey="Home" ;; - end) baseKey="End" ;; - insert) baseKey="Insert" ;; - delete) baseKey="Delete" ;; - pageup) baseKey="Page_Up" ;; - pagedown) baseKey="Page_Down" ;; - *) baseKey="$input" ;; - esac - - echo "${result}${baseKey}" -} +select_personal_mode_key() { + local ratpoisonModeKeys=( + "Control+t" + "Control+z" + "Control+Escape" + "Alt+Escape" + "Control+space" + "Super Left" + "Super Right" + ) + local personalModeKeyOptions=() + local option normalizedOption -populateUsedKeys() { - # Populate with existing ratpoison mode bindings - usedKeys["Shift+slash"]=1 - usedKeys["c"]=1 - usedKeys["e"]=1 - usedKeys["f"]=1 - usedKeys["\$mod+e"]=1 - usedKeys["w"]=1 - usedKeys["k"]=1 - usedKeys["m"]=1 - usedKeys["Print"]=1 - usedKeys["\$mod+r"]=1 - usedKeys["p"]=1 - usedKeys["\$mod+s"]=1 - usedKeys["Mod1+Shift+0"]=1 - usedKeys["Mod1+Shift+9"]=1 - usedKeys["Mod1+Shift+equal"]=1 - usedKeys["Mod1+Shift+minus"]=1 - usedKeys["Mod1+Shift+z"]=1 - usedKeys["Mod1+Shift+c"]=1 - usedKeys["Mod1+Shift+x"]=1 - usedKeys["Mod1+Shift+v"]=1 - usedKeys["Mod1+Shift+b"]=1 - usedKeys["Mod1+Shift+u"]=1 - usedKeys["Mod1+b"]=1 - usedKeys["g"]=1 - usedKeys["apostrophe"]=1 - usedKeys["Shift+c"]=1 - usedKeys["Shift+o"]=1 - usedKeys["Shift+t"]=1 - usedKeys["Control+semicolon"]=1 - usedKeys["Control+Shift+semicolon"]=1 - usedKeys["Shift+exclam"]=1 - usedKeys["\$mod+q"]=1 - usedKeys["Control+\$mod+q"]=1 - usedKeys["Escape"]=1 - usedKeys["Control+g"]=1 -} - -inputText() { - # Args: prompt text - dialog --title "I38" --inputbox "$1" 0 0 --stdout -} - -addCustomApplication() { - local appName appCommand appFlags keybinding convertedKey - - populateUsedKeys - - while true; do - appName="$(inputText "Custom Applications:\n\nEnter application name (or press enter when finished):")" - [[ -z "$appName" ]] && break - - appCommand="$(inputText "Enter execution path/command for $appName:")" - [[ -z "$appCommand" ]] && continue - - appFlags="$(inputText "Enter command line flags for $appName (optional):")" - - while true; do - keybinding="$(inputText "Enter keybinding for $appName (Examples: c, ^c, !f1, mspace, ^!up) or ? for help:")" - - if [[ "$keybinding" == "?" ]]; then - showKeybindingHelp - continue - fi - - [[ -z "$keybinding" ]] && break - - convertedKey="$(convertKeybinding "$keybinding")" - - if [[ -n "${usedKeys[$convertedKey]}" ]]; then - dialog --title "I38" --msgbox "Keybinding '$keybinding' ($convertedKey) is already in use. Please choose another." 0 0 - continue - fi - - # Add to arrays - customApps+=("$appName|$appCommand|$appFlags|$convertedKey") - usedKeys["$convertedKey"]=1 - break - done + for option in "${ratpoisonModeKeys[@]}"; do + normalizedOption="$(normalize_ratpoison_key "$option")" + if [[ "$normalizedOption" != "$escapeKey" ]]; then + personalModeKeyOptions+=("$option") + fi done + + if [[ ${#personalModeKeyOptions[@]} -eq 0 ]]; then + echo "" + return + fi + + local selectedKey + selectedKey="$(menulist "Personal mode key:" "${personalModeKeyOptions[@]}")" + normalize_ratpoison_key "$selectedKey" +} + +update_personal_customizations() { + local customizationsPath="${i3Path}/customizations" + local startMarker="# I38 Personal mode start" + local endMarker="# I38 Personal mode end" + local personalModeBlock + + if personal_mode_exists; then + return + fi + + if [[ "${personalModeEnabled:-1}" -ne 0 ]] || [[ -z "$personalModeKey" ]]; then + return + fi + + personalModeBlock=$(cat << EOF +${startMarker} +bindsym ${personalModeKey} mode "personal" +# A template mode where you can bind items that will not be overwritten during updates +mode "personal" { + bindsym F1 exec ${i3Path}/scripts/i38-help-personal.sh, mode "default" + bindsym Escape mode "default" + bindsym Control+g mode "default" +} +${endMarker} +EOF +) + + if [[ -f "$customizationsPath" ]] && [[ -s "$customizationsPath" ]]; then + printf "\n%s\n" "$personalModeBlock" >> "$customizationsPath" + else + printf "%s\n" "$personalModeBlock" > "$customizationsPath" + fi +} + +personal_mode_exists() { + local customizationsPath="${i3Path}/customizations" + + [[ -f "$customizationsPath" ]] && grep -q 'mode "personal"' "$customizationsPath" } load_config() { @@ -347,20 +271,6 @@ load_config() { IFS=' ' read -ra kbd <<< "$kbd" fi - # Reconstruct customApps array from numbered entries - customApps=() - local i=0 - local varName - while : ; do - varName="customApp_$i" - if [[ -n "${!varName}" ]]; then - customApps+=("${!varName}") - ((i++)) - else - break - fi - done - return 0 fi return 1 @@ -398,12 +308,13 @@ batteryAlert="${batteryAlert:-1}" brlapi="$brlapi" sounds="$sounds" -# Custom applications +# Screen lock +screenlockPinHash="$screenlockPinHash" + +# Personal mode +personalModeEnabled="${personalModeEnabled:-1}" +personalModeKey="$personalModeKey" EOF - # Save custom apps with numbered keys - for i in "${!customApps[@]}"; do - echo "customApp_$i=\"${customApps[$i]}\"" >> "$configFile" - done dialog --title "I38" --msgbox "Configuration saved to $configFile\n\nYou can edit this file manually or delete it to reconfigure from scratch." 0 0 fi @@ -429,6 +340,11 @@ write_xinitrc() if [[ -f "$HOME/.xinitrc" ]]; then yesno "This will overwrite your existing $HOME/.xinitrc file. Do you want to continue?" || exit 0 fi +if yesno "Do you want to launch i3 without an isolated D-Bus session? Selecting No will keep the dbus-session-launch wrapper."; then + sessionCommand="exec -- i3" +else + sessionCommand="exec dbus-session-launch -- i3" +fi cat << 'EOF' > ~/.xinitrc #!/bin/sh # @@ -436,10 +352,11 @@ cat << 'EOF' > ~/.xinitrc # # Executed by startx (run your window manager from here) -[[ -f ~/.Xresources ]] && xrdb -merge -I$HOME ~/.Xresources +[[ -f ~/.Xresources ]] && xrdb -merge -I\$HOME ~/.Xresources if [ -d /etc/X11/xinit/xinitrc.d ]; then for f in /etc/X11/xinit/xinitrc.d/*; do + # shellcheck disable=SC2154 [ -x "$f" ] && . "$f" done unset f @@ -447,9 +364,8 @@ fi [ -f /etc/xprofile ] && . /etc/xprofile [ -f ~/.xprofile ] && . ~/.xprofile - -exec dbus-run-session -- i3 EOF +echo "$sessionCommand" >> ~/.xinitrc chmod +x ~/.xinitrc } @@ -472,8 +388,35 @@ EOF exit 0 } +apply_screenlock_pin() { + local pinFile="${i3Path}/.screenpin" + local pinValue="$screenlockPinHash" + + if [[ -z "$pinValue" ]]; then + rm -f "$pinFile" + return 0 + fi + + printf "%s\n" "$pinValue" > "$pinFile" + chmod 600 "$pinFile" +} + update_scripts() { + local existingPinHash="" + local pinFile="${i3Path}/.screenpin" + if [[ -f "$pinFile" ]]; then + read -r existingPinHash < "$pinFile" + fi + if [[ -z "$existingPinHash" ]] && [[ -f "$configFile" ]]; then + # shellcheck source=/dev/null + source "$configFile" + existingPinHash="$screenlockPinHash" + fi cp -rv scripts/ "${i3Path}/" | dialog --backtitle "I38" --progressbox "Updating scripts..." -1 -1 + if [[ -n "$existingPinHash" ]]; then + screenlockPinHash="$existingPinHash" + apply_screenlock_pin + fi exit 0 } @@ -589,132 +532,114 @@ if [[ -z "$volumeJump" ]]; then fi # Screen Reader if [[ -z "$screenReader" ]] || ! command -v "$screenReader" &> /dev/null; then - unset programList + programList=() for i in cthulhu orca ; do - if command -v ${i/#-/} &> /dev/null ; then - if [ -n "$programList" ]; then - programList="$programList $i" - else - programList="$i" - fi + if command -v "${i/#-/}" &> /dev/null ; then + programList+=("$i") fi done - if [ "$programList" != "${programList// /}" ]; then - screenReader="$(menulist ":Screen Reader" $programList)" + if [[ ${#programList[@]} -gt 1 ]]; then + screenReader="$(menulist ":Screen Reader" "${programList[@]}")" else - screenReader="${programList/#-/}" + screenReader="${programList[0]#-}" fi - export screenReader="$(command -v $screenReader)" + screenReader="$(command -v "$screenReader")" + export screenReader else # Validate and export existing preference export screenReader fi # Email client if [[ -z "$emailClient" ]] || ! command -v "$emailClient" &> /dev/null; then - unset programList + programList=() for i in betterbird evolution thunderbird ; do - if command -v ${i/#-/} &> /dev/null ; then - if [ -n "$programList" ]; then - programList="$programList $i" - else - programList="$i" - fi + if command -v "${i/#-/}" &> /dev/null ; then + programList+=("$i") fi done - if [ "$programList" != "${programList// /}" ]; then - emailClient="$(menulist "Email client:" $programList)" + if [[ ${#programList[@]} -gt 1 ]]; then + emailClient="$(menulist "Email client:" "${programList[@]}")" else - emailClient="${programList/#-/}" + emailClient="${programList[0]#-}" fi - export emailClient="$(command -v $emailClient)" + emailClient="$(command -v "$emailClient")" + export emailClient else # Validate and export existing preference export emailClient fi # Web browser if [[ -z "$webBrowser" ]] || ! command -v "$webBrowser" &> /dev/null; then - unset programList + programList=() for i in brave chromium epiphany firefox google-chrome-stable google-chrome-unstable microsoft-edge-stable microsoft-edge-beta microsoft-edge-dev midori seamonkey vivaldi ; do - if command -v ${i/#-/} &> /dev/null ; then - if [ -n "$programList" ]; then - programList="$programList $i" - else - programList="$i" - fi + if command -v "${i/#-/}" &> /dev/null ; then + programList+=("$i") fi done - if [ "$programList" != "${programList// /}" ]; then - webBrowser="$(menulist "Web browser:" $programList)" + if [[ ${#programList[@]} -gt 1 ]]; then + webBrowser="$(menulist "Web browser:" "${programList[@]}")" else - webBrowser="${programList/#-/}" + webBrowser="${programList[0]#-}" fi - export webBrowser="$(command -v $webBrowser)" + webBrowser="$(command -v "$webBrowser")" + export webBrowser else # Validate and export existing preference export webBrowser fi # Text editor if [[ -z "$textEditor" ]] || ! command -v "$textEditor" &> /dev/null; then - unset programList + programList=() for i in emacs geany gedit kate kwrite l3afpad leafpad libreoffice mousepad pluma ; do - if hash ${i/#-/} &> /dev/null ; then - if [ -n "$programList" ]; then - programList="$programList $i" - else - programList="$i" - fi + if command -v "${i/#-/}" &> /dev/null ; then + programList+=("$i") fi done - if [ "$programList" != "${programList// /}" ]; then - textEditor="$(menulist "Text editor:" $programList)" + if [[ ${#programList[@]} -gt 1 ]]; then + textEditor="$(menulist "Text editor:" "${programList[@]}")" else - textEditor="${programList/#-/}" + textEditor="${programList[0]#-}" fi - export textEditor="$(command -v $textEditor)" + textEditor="$(command -v "$textEditor")" + export textEditor else # Validate and export existing preference export textEditor fi # File browser if [[ -z "$fileBrowser" ]] || ! command -v "$fileBrowser" &> /dev/null; then - unset programList + programList=() for i in caja nemo nautilus pcmanfm pcmanfm-qt thunar ; do - if hash ${i/#-/} &> /dev/null ; then - if [ -n "$programList" ]; then - programList="$programList $i" - else - programList="$i" - fi + if command -v "${i/#-/}" &> /dev/null ; then + programList+=("$i") fi done - if [ "$programList" != "${programList// /}" ]; then - fileBrowser="$(menulist "File browser:" $programList)" + if [[ ${#programList[@]} -gt 1 ]]; then + fileBrowser="$(menulist "File browser:" "${programList[@]}")" else - fileBrowser="${programList/#-/}" + fileBrowser="${programList[0]#-}" fi - export fileBrowser="$(command -v $fileBrowser)" + fileBrowser="$(command -v "$fileBrowser")" + export fileBrowser else # Validate and export existing preference export fileBrowser fi # IRC client if [[ -z "$ircClient" ]] || ! command -v "$ircClient" &> /dev/null; then - unset programList + programList=() for i in albikirc Albikirc access-irc ; do - if command -v ${i/#-/} &> /dev/null ; then - if [ -n "$programList" ]; then - programList="$programList $i" - else - programList="$i" - fi + if command -v "${i/#-/}" &> /dev/null ; then + programList+=("$i") fi done - if [ "$programList" != "${programList// /}" ]; then - ircClient="$(menulist "IRC client:" $programList)" + if [[ ${#programList[@]} -gt 1 ]]; then + ircClient="$(menulist "IRC client:" "${programList[@]}")" else - ircClient="${programList/#-/}" + ircClient="${programList[0]#-}" fi - export ircClient="$(command -v $ircClient)" + ircClient="$(command -v "$ircClient")" + export ircClient else # Validate and export existing preference export ircClient @@ -742,7 +667,7 @@ if [[ -z "$dex" ]]; then fi fi if [[ $dex -eq 0 ]]; then - dex -t "${XDG_CONFIG_HOME:-${HOME}/.config}/autostart" -c $(command -v $screenReader) + dex -t "${XDG_CONFIG_HOME:-${HOME}/.config}/autostart" -c "$(command -v "$screenReader")" fi if [[ -z "$batteryAlert" ]]; then if command -v acpi &> /dev/null ; then @@ -767,9 +692,65 @@ if [[ -z "$sounds" ]]; then sounds=1 fi fi -# Custom applications for ratpoison mode -if [[ ${#customApps[@]} -eq 0 ]]; then - addCustomApplication +if [[ -z "$screenlockPinHash" ]]; then + screenlockPinFile="${i3Path}/.screenpin" + if [[ -f "$screenlockPinFile" ]]; then + read -r screenlockPinHash < "$screenlockPinFile" + fi +fi +if [[ -z "$screenlockPinHash" ]]; then + if yesno "Do you want to enable the I38 screen lock (privacy screen only, not a secure system lock)?"; then + while : ; do + screenlockPin="$(dialog --title "I38" --clear --passwordbox "Enter a 4-digit PIN to enable screen lock." -1 -1 --stdout)" + dialogResult=$? + if [[ $dialogResult -ne 0 ]]; then + screenlockPinHash="" + break + fi + if [[ ! "$screenlockPin" =~ ^[0-9]{4}$ ]]; then + dialog --title "I38" --msgbox "PIN must be exactly 4 digits." -1 -1 + continue + fi + + screenlockPinConfirm="$(dialog --title "I38" --clear --passwordbox "Re-enter the 4-digit PIN to confirm." -1 -1 --stdout)" + dialogResult=$? + if [[ $dialogResult -ne 0 ]]; then + screenlockPinHash="" + break + fi + if [[ "$screenlockPin" != "$screenlockPinConfirm" ]]; then + dialog --title "I38" --msgbox "PINs do not match. Please try again." -1 -1 + continue + fi + + screenlockPinHash="$(printf "%s" "$screenlockPin" | sha512sum | awk '{print $1}')" + unset screenlockPin screenlockPinConfirm + break + done + fi +fi +# Personal mode +personalModeExists=1 +if personal_mode_exists; then + personalModeExists=0 + personalModeEnabled=0 +fi + +if [[ $personalModeExists -ne 0 ]]; then + if yesno "Would you like a Personal mode?"; then + personalModeEnabled=0 + else + personalModeEnabled=1 + fi + + if [[ "$personalModeEnabled" -eq 0 ]]; then + if [[ -z "$personalModeKey" ]] || [[ "$personalModeKey" == "$escapeKey" ]]; then + personalModeKey="$(select_personal_mode_key)" + fi + if [[ -z "$personalModeKey" ]]; then + personalModeEnabled=1 + fi + fi fi # Save configuration if requested (only on first run) @@ -788,8 +769,10 @@ write_waytray_config mkdir -p "${i3Path}" # Move scripts into place cp -rv scripts/ "${i3Path}/" | dialog --backtitle "I38" --progressbox "Moving scripts into place and writing config..." -1 -1 +apply_screenlock_pin +update_personal_customizations -cat << EOF > ${i3Path}/config +cat << EOF > "${i3Path}/config" # Generated by I38 (${0##*/}) https://git.stormux.org/storm/I38 # $(date '+%A, %B %d, %Y at %I:%M%p') EOF @@ -797,13 +780,13 @@ EOF # If we are using Sway, we need to load in the system configuration # Usually, this is for system specific dBus things that the distro knows how to manage; we should trust their judgment with that if [[ $usingSway ]] && [[ -d "${swaySystemIncludesPath}" ]]; then - cat << EOF >> ${i3Path}/config + cat << EOF >> "${i3Path}/config" # Include your distribution Sway configuration files. include ${swaySystemIncludesPath}/* EOF fi -cat << EOF >> ${i3Path}/config +cat << EOF >> "${i3Path}/config" # i3 config file (v4) # # Please see https://i3wm.org/docs/userguide.html for a complete reference! @@ -972,18 +955,29 @@ bindsym $mod+Shift+BackSpace mode "default" EOF +if [[ -n "$screenlockPinHash" ]]; then + cat << EOF >> "${i3Path}/config" +# Screen lock mode (managed by screenlock.sh) +mode "screenlock" { + bindsym Escape nop + bindsym Control+g nop +} + +EOF +fi + # Perform OCR on screen -echo "bindsym ${mod}+F5 exec ${i3Path}/scripts/ocr.py" >> ${i3Path}/config +echo "bindsym ${mod}+F5 exec ${i3Path}/scripts/ocr.py" >> "${i3Path}/config" # Interrupt speech output -echo "bindsym ${mod}+Shift+F5 exec spd-say -C" >> ${i3Path}/config +echo "bindsym ${mod}+Shift+F5 exec spd-say -C" >> "${i3Path}/config" # Multiple keyboard layouts if requested. if [[ ${#kbd[@]} -gt 1 ]]; then - echo "bindsym Mod4+space exec ${i3Path}/scripts/keyboard.sh cycle ${kbd[*]}" >> ${i3Path}/config + echo "bindsym Mod4+space exec ${i3Path}/scripts/keyboard.sh cycle ${kbd[*]}" >> "${i3Path}/config" fi # Create panel mode -cat << EOF >> ${i3Path}/config +cat << EOF >> "${i3Path}/config" # Panel mode configuration bindsym Control+Mod1+Tab mode "panel" mode "panel" { @@ -1023,9 +1017,13 @@ fi) # Detailed battery information bound to Shift+b bindsym Shift+b exec --no-startup-id ${i3Path}/scripts/battery_status.sh --detailed, mode "default" -$(if command -v lxsession-logout &> /dev/null ; then - echo "# Power options bound to p" - echo "bindsym p exec --no-startup-id lxsession-logout, mode \"default\"" + # Power options bound to p + bindsym p exec --no-startup-id ${i3Path}/scripts/power.sh, mode "default" + +$(if [[ -n "$screenlockPinHash" ]]; then + echo " # Screen lock (privacy screen only)" + echo " bindsym Control+\$mod+l exec --no-startup-id ${i3Path}/scripts/screenlock.sh, mode \"default\"" + echo " " fi) # Exit panel mode without any action @@ -1036,7 +1034,7 @@ EOF # Create ratpoison mode if requested. if [[ -n "${escapeKey}" ]]; then - cat << EOF >> ${i3Path}/config + cat << EOF >> "${i3Path}/config" # Enter ratpoison mode bindsym $escapeKey mode "ratpoison" mode "ratpoison" { @@ -1107,24 +1105,14 @@ bindsym Shift+o exec $(command -v orca) --replace, mode "default" bindsym Shift+t exec ${i3Path}/scripts/toggle_screenreader.sh, mode "default" $(if [[ $usingSway -eq 0 ]]; then echo "# reload the configuration file" - echo "bindsym Control+semicolon exec bash -c '$i3msg -t command reload && spd-say -P important -Cw "I38 Configuration reloaded."', mode "default"" + echo "bindsym Control+semicolon exec bash -c '$i3msg -t command reload && spd-say -P important -Cw \"I38 Configuration reloaded.\"', mode \"default\"" else echo "# reload the configuration file" - echo "bindsym Control+semicolon exec bash -c '$i3msg -t run_command reload && spd-say -P important -Cw "I38 Configuration reloaded."', mode "default"" + echo "bindsym Control+semicolon exec bash -c '$i3msg -t run_command reload && spd-say -P important -Cw \"I38 Configuration reloaded.\"', mode \"default\"" echo "# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)" - echo "bindsym Control+Shift+semicolon exec $i3msg -t run_command restart && spd-say -P important -Cw "I3 restarted.", mode "default"" + echo "bindsym Control+Shift+semicolon exec $i3msg -t run_command restart && spd-say -P important -Cw \"I3 restarted.\", mode \"default\"" fi) -# Custom applications -$(for app in "${customApps[@]}"; do - IFS='|' read -r appName appCommand appFlags appKey <<< "$app" - echo "# $appName bound to $appKey" - if [[ -n "$appFlags" ]]; then - echo "bindsym $appKey exec $appCommand $appFlags, mode \"default\"" - else - echo "bindsym $appKey exec $appCommand, mode \"default\"" - fi -done) # Run dialog with exclamation bindsym Shift+exclam exec ${i3Path}/scripts/run_dialog.sh, mode "default" # exit i3 (logs you out of your X session) @@ -1140,7 +1128,7 @@ EOF fi -cat << EOF >> ${i3Path}/config +cat << EOF >> "${i3Path}/config" # Auto start section $(if [[ $sounds -eq 0 ]]; then if [[ $usingSway -eq 0 ]]; then diff --git a/scripts/i38-help-personal.sh b/scripts/i38-help-personal.sh new file mode 100755 index 0000000..422d8d0 --- /dev/null +++ b/scripts/i38-help-personal.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +# This file is part of I38. + +# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. + +# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with I38. If not, see . + +configPath="$(readlink -f "$0")" +configPath="${configPath%/*/*}" +customizationsPath="${configPath}/customizations" + +if [[ -f "${configPath}/config" ]]; then + mod="$(grep "set \$mod " "${configPath}/config" | cut -d ' ' -f3)" + mod="${mod//Mod1/Alt}" + mod="${mod//Mod4/Super}" +else + exit 1 +fi + +if [[ ! -f "$customizationsPath" ]] || ! grep -q '^mode "personal"' "$customizationsPath"; then + message="Personal mode bindings were not found. Check ${customizationsPath}." + echo -e "$message" | yad --text-info --show-cursor --title "I38 - Personal Mode Help" --button "Close:0" --listen + exit 0 +fi + +personalModeKey="$(grep -m1 -E '^bindsym[[:space:]]+.*mode "personal"' "$customizationsPath" | \ + sed -e 's/^bindsym[[:space:]]*//' -e 's/[[:space:]]*mode "personal".*$//')" +if [[ -n "$personalModeKey" ]]; then + personalModeKey="${personalModeKey//\$mod/$mod}" + personalModeKey="${personalModeKey//Mod1/Alt}" + personalModeKey="${personalModeKey//Mod4/Super}" +fi + +mapfile helpText < <(sed -n '/^mode "personal"/,/^}$/p' "$customizationsPath" | \ + sed -e '/^mode "personal"/d' \ + -e '/^}$/d' \ + -e 's/bindsym/Key:/g' \ + -e 's/Mod1/Alt/g' \ + -e 's/, mode "default"//g' \ + -e 's/--no-startup-id //g' \ + -e "s/\$mod/$mod/g") + +for i in "${!helpText[@]}" ; do + helpText[i]="${helpText[i]//${configPath}\/scripts\//}" + helpText[i]="${helpText[i]/.sh/}" + helpText[i]="${helpText[i]/.py/}" +done + +header="Personal Mode Keybindings\n\n" +if [[ -n "$personalModeKey" ]]; then + header+="Press ${personalModeKey} to enter personal mode, then use these shortcuts:\n\n" +else + header+="Use these shortcuts while in personal mode:\n\n" +fi + +helpText=("$header" "${helpText[@]}" "End of personal mode help. Press Control+Home to jump to the beginning.") + +echo -e "${helpText[@]}" | yad --text-info --show-cursor --title "I38 - Personal Mode Help" --button "Close:0" --listen + +exit 0 diff --git a/scripts/power.sh b/scripts/power.sh new file mode 100755 index 0000000..f46fd08 --- /dev/null +++ b/scripts/power.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +# This file is part of I38. + +# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. + +# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with I38. If not, see . + +if ! command -v yad &> /dev/null; then + echo "yad is required for power options." + exit 1 +fi + +action="$(yad --title "I38" --text "Power options" --list \ + --column "Action" --column "Description" \ + "Power off" "Shut down the system" \ + "Reboot" "Restart the system" \ + "Log out" "Exit the current session" \ + --print-column=1 --separator="" --button "Select:0" --button "Cancel:1")" + +yadResult=$? +if [[ $yadResult -ne 0 ]]; then + exit 0 +fi + +case "$action" in + "Power off") + powerAction="poweroff" + login1Method="PowerOff" + consolekitMethod="Stop" + ;; + "Reboot") + powerAction="reboot" + login1Method="Reboot" + consolekitMethod="Restart" + ;; + "Log out") + powerAction="logout" + ;; + *) + exit 0 + ;; + esac + +confirmAction() { + local promptText="$1" + yad --title "I38" --text "$promptText" --button "Yes:0" --button "No:1" + return $? +} + +case "$powerAction" in + "poweroff") + confirmAction "Power off the system now?" || exit 0 + ;; + "reboot") + confirmAction "Reboot the system now?" || exit 0 + ;; + "logout") + confirmAction "Log out of the current session now?" || exit 0 + ;; +esac + +try_logout() { + if [[ -n "${SWAYSOCK:-}" ]] && command -v swaymsg &> /dev/null; then + swaymsg -t command exit &> /dev/null + return $? + fi + if [[ -n "${I3SOCK:-}" ]] && command -v i3-msg &> /dev/null; then + i3-msg -t command exit &> /dev/null + return $? + fi + if command -v swaymsg &> /dev/null; then + swaymsg -t command exit &> /dev/null + return $? + fi + if command -v i3-msg &> /dev/null; then + i3-msg -t command exit &> /dev/null + return $? + fi + return 1 +} + +try_loginctl() { + command -v loginctl &> /dev/null || return 1 + loginctl "$powerAction" --no-ask-password &> /dev/null +} + +try_systemctl() { + command -v systemctl &> /dev/null || return 1 + systemctl "$powerAction" --no-ask-password &> /dev/null +} + +try_gdbus_login1() { + command -v gdbus &> /dev/null || return 1 + gdbus call --system \ + --dest org.freedesktop.login1 \ + --object-path /org/freedesktop/login1 \ + --method "org.freedesktop.login1.Manager.${login1Method}" false \ + &> /dev/null +} + +try_gdbus_consolekit() { + command -v gdbus &> /dev/null || return 1 + gdbus call --system \ + --dest org.freedesktop.ConsoleKit \ + --object-path /org/freedesktop/ConsoleKit/Manager \ + --method "org.freedesktop.ConsoleKit.Manager.${consolekitMethod}" \ + &> /dev/null +} + +try_dbus_send_login1() { + command -v dbus-send &> /dev/null || return 1 + dbus-send --system --print-reply \ + --dest=org.freedesktop.login1 \ + /org/freedesktop/login1 \ + "org.freedesktop.login1.Manager.${login1Method}" \ + boolean:false \ + &> /dev/null +} + +try_dbus_send_consolekit() { + command -v dbus-send &> /dev/null || return 1 + dbus-send --system --print-reply \ + --dest=org.freedesktop.ConsoleKit \ + /org/freedesktop/ConsoleKit/Manager \ + "org.freedesktop.ConsoleKit.Manager.${consolekitMethod}" \ + &> /dev/null +} + +try_shutdown() { + command -v shutdown &> /dev/null || return 1 + if [[ "$powerAction" == "poweroff" ]]; then + shutdown -h now &> /dev/null + else + shutdown -r now &> /dev/null + fi +} + +try_direct() { + command -v "$powerAction" &> /dev/null || return 1 + "$powerAction" &> /dev/null +} + +if [[ "$powerAction" == "logout" ]]; then + if try_logout; then + exit 0 + fi +else + if try_loginctl || try_systemctl || try_gdbus_login1 || try_dbus_send_login1 || \ + try_gdbus_consolekit || try_dbus_send_consolekit || try_shutdown || try_direct; then + exit 0 + fi +fi + +yad --title "I38" --text "Power action failed. You may need permission or a polkit agent to continue." --button "Close:0" +exit 1 diff --git a/scripts/screenlock.sh b/scripts/screenlock.sh new file mode 100755 index 0000000..79f29ed --- /dev/null +++ b/scripts/screenlock.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +# This file is part of I38. + +# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. + +# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with I38. If not, see . + +screenlockPinHash="" +scriptPath="$(readlink -f "$0")" +scriptDir="${scriptPath%/*}" +i3Path="${scriptDir%/scripts}" +pinFile="${i3Path}/.screenpin" + +if [[ -f "$pinFile" ]]; then + read -r screenlockPinHash < "$pinFile" +fi + +if [[ -z "$screenlockPinHash" ]]; then + yad --title "I38" --text "Screen lock is not configured. Run i38.sh and set a 4-digit PIN to enable it." --button "Close:0" + exit 1 +fi + +if ! command -v yad &> /dev/null; then + exit 1 +fi + +if ! command -v jq &> /dev/null; then + yad --title "I38" --text "Screen lock requires jq to determine the current workspace." --button "Close:0" + exit 1 +fi + +if ! command -v sha512sum &> /dev/null; then + yad --title "I38" --text "Screen lock requires sha512sum to validate the PIN." --button "Close:0" + exit 1 +fi + +wmMsg="i3-msg" +if [[ -n "${SWAYSOCK:-}" ]] && command -v swaymsg &> /dev/null; then + wmMsg="swaymsg" +elif [[ -n "${I3SOCK:-}" ]] && command -v i3-msg &> /dev/null; then + wmMsg="i3-msg" +elif command -v swaymsg &> /dev/null; then + wmMsg="swaymsg" +elif command -v i3-msg &> /dev/null; then + wmMsg="i3-msg" +else + yad --title "I38" --text "No i3 or sway command interface was found for screen lock." --button "Close:0" + exit 1 +fi + +currentWorkspace="$($wmMsg -t get_workspaces | jq -r '.[] | select(.focused==true) | .name')" +lockWorkspace="i38-lock" +if $wmMsg -t get_workspaces | jq -e --arg name "$lockWorkspace" '.[] | select(.name==$name)' &> /dev/null; then + lockWorkspace="i38-lock-$$" +fi + +$wmMsg -t command "workspace --no-auto-back-and-forth \"$lockWorkspace\"" &> /dev/null +$wmMsg -t command "mode screenlock" &> /dev/null + +attemptCount=0 +while : ; do + if [[ $attemptCount -eq 0 ]]; then + promptText="Screen lock is enabled. Enter your 4-digit PIN to unlock." + else + promptText="Incorrect PIN. Enter your 4-digit PIN to unlock." + fi + + pinInput="$(yad --entry --hide-text --title "I38" --text "$promptText" --entry-label "Screen lock PIN" --button "Unlock:0" --on-top --sticky --skip-taskbar --fixed --center --undecorated --fullscreen --no-escape)" + yadResult=$? + if [[ $yadResult -ne 0 ]]; then + attemptCount=$((attemptCount + 1)) + continue + fi + + if [[ ! "$pinInput" =~ ^[0-9]{4}$ ]]; then + attemptCount=$((attemptCount + 1)) + continue + fi + + pinHash="$(printf "%s" "$pinInput" | sha512sum | awk '{print $1}')" + unset pinInput + + if [[ "$pinHash" == "$screenlockPinHash" ]]; then + break + fi + + attemptCount=$((attemptCount + 1)) +done + +$wmMsg -t command "mode default" &> /dev/null +if [[ -n "$currentWorkspace" ]]; then + $wmMsg -t command "workspace \"$currentWorkspace\"" &> /dev/null +fi + +exit 0 diff --git a/scripts/sound.py b/scripts/sound.py index 22e94e6..9e7ffd8 100755 --- a/scripts/sound.py +++ b/scripts/sound.py @@ -71,12 +71,16 @@ def on_mode(self,event): mode = event.change if mode == 'ratpoison': play_sound_async('play -qV0 "|sox -np synth .07 sq 400" "|sox -np synth .5 sq 800" fade h 0 .5 .5 norm -20') + elif mode == 'personal': + play_sound_async('play -nqV0 synth pl E3 pl B3 remix - fade h 0 .25 .2 overdrive riaa norm -12') elif mode == 'bypass': play_sound_async('play -nqV0 synth .1 saw 700 saw 1200 delay 0 .04 remix - norm -6') elif mode == 'default': # Play different sounds based on which mode we're exiting if currentMode == 'ratpoison': play_sound_async('play -qV0 "|sox -np synth .07 sq 400" "|sox -np synth .5 sq 800" fade h 0 .5 .5 norm -20 reverse') + elif currentMode == 'personal': + play_sound_async('play -nqV0 synth pl E3 pl B3 remix - fade h 0 .25 .2 overdrive riaa norm -12 reverse') elif currentMode == 'panel': play_sound_async('play -nqV0 synth 0.05 pluck C5 norm -8 : synth 0.05 pluck F4 norm -8 : synth 0.05 pluck C4 norm -8 : synth 0.05 pluck F3 norm -8') elif currentMode == 'bypass':