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':