Files

1914 lines
65 KiB
Bash
Executable File

#!/usr/bin/env bash
# Configures the i3 window manager to make it screen reader accessible
# Written by Storm Dragon, Jeremiah, and contributers.
# Copyright Stormux, 2022
# This program 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.
# This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
i3Path="${XDG_CONFIG_HOME:-$HOME/.config}/i3"
i3msg="i3-msg"
configFile="${PWD}/I38_preferences.conf"
i38VersionFile="${i3Path}/i38-version"
i38CheckoutPath="${PWD}"
defaultConfig=0
defaultGenerateX=0
# Dialog accessibility
export DIALOGOPTS='--no-lines --visit-items'
# Check to make sure minimum requirements are installed.
declare -a missing=()
for i in dialog jq python3 rsync yad xdotool ; do
if ! command -v "$i" &> /dev/null ; then
missing+=("$i")
fi
done
if ! command -v xdg-open &> /dev/null ; then
missing+=("xdg-utils")
fi
if command -v python3 &> /dev/null ; then
if ! python3 -c 'import i3ipc' &> /dev/null ; then
missing+=("python-i3ipc")
fi
if ! python3 -c 'import gi; gi.require_version("Atk", "1.0"); gi.require_version("Gdk", "3.0"); gi.require_version("Gio", "2.0"); gi.require_version("GLib", "2.0"); gi.require_version("Gtk", "3.0"); from gi.repository import Atk, Gdk, Gio, GLib, Gtk' &> /dev/null ; then
missing+=("python-gobject")
fi
fi
if [[ ${#missing[@]} -gt 0 ]]; then
echo "Please install the following packages and run this script again:"
echo "${missing[*]}"
exit 1
fi
if ! command -v pandoc &> /dev/null && ! command -v markdown &> /dev/null ; then
echo "Please install either pandoc or markdown."
echo "The markdown command may be provided by the package discount."
exit 1
fi
keyboard_menu() {
keyboardMenu=("us" "English (US)"
"af" "Dari"
"al" "Albanian"
"et" "Amharic"
"ara" "Arabic"
"ma" "Arabic (Morocco)"
"sy" "Arabic (Syria)"
"am" "Armenian"
"az" "Azerbaijani"
"ml" "Bambara"
"bd" "Bangla"
"by" "Belarusian"
"be" "Belgian"
"dz" "Berber (Algeria, Latin)"
"ba" "Bosnian"
"brai" "Braille"
"bg" "Bulgarian"
"mm" "Burmese"
"cn" "Chinese"
"hr" "Croatian"
"cz" "Czech"
"dk" "Danish"
"mv" "Dhivehi"
"nl" "Dutch"
"bt" "Dzongkha"
"au" "English (Australia)"
"gh" "English (Ghana)"
"ng" "English (Nigeria)"
"za" "English (South Africa)"
"gb" "English (UK)"
"epo" "Esperanto"
"ee" "Estonian"
"fo" "Faroese"
"ph" "Filipino"
"fi" "Finnish"
"fr" "French"
"ca" "French (Canada)"
"cd" "French (Democratic Republic of the Congo)"
"ge" "Georgian"
"de" "German"
"ch" "German (Switzerland)"
"gr" "Greek"
"il" "Hebrew"
"hu" "Hungarian"
"is" "Icelandic"
"in" "Indian"
"jv" "Indonesian (Javanese)"
"iq" "Iraqi"
"ie" "Irish"
"it" "Italian"
"jp" "Japanese"
"nec_vndr/jp" "Japanese (PC-98)"
"kz" "Kazakh"
"kh" "Khmer (Cambodia)"
"kr" "Korean"
"kg" "Kyrgyz"
"la" "Lao"
"lv" "Latvian"
"lt" "Lithuanian"
"mk" "Macedonian"
"mv" "Malay (Jawi, Arabic Keyboard)"
"mt" "Maltese"
"mao" "Maori"
"mn" "Mongolian"
"me" "Montenegrin"
"gn" "N'Ko (AZERTY)"
"np" "Nepali"
"no" "Norwegian"
"ir" "Persian"
"pl" "Polish"
"pt" "Portuguese"
"br" "Portuguese (Brazil)"
"ro" "Romanian"
"ru" "Russian"
"rs" "Serbian"
"lk" "Sinhala (phonetic)"
"sk" "Slovak"
"si" "Slovenian"
"es" "Spanish"
"latam" "Spanish (Latin American)"
"tz" "Swahili (Tanzania)"
"se" "Swedish"
"tw" "Taiwanese"
"tj" "Tajik"
"th" "Thai"
"tr" "Turkish"
"tm" "Turkmen"
"bw" "Tswana"
"ua" "Ukrainian"
"pk" "Urdu (Pakistan)"
"uz" "Uzbek (Afghanistan)"
"vn" "Vietnamese"
"sn" "Wolof"
)
dialog --title "I38" \
--backtitle "Use the arrow keys to find the option you want, and enter to select it. When you are finished selecting layouts, use right arrow to find \"Done\" and press enter." \
--clear \
--cancel-label "Done" \
--no-tags \
--menu "Select Keyboard Layout" 0 0 0 "${keyboardMenu[@]}" --stdout
return $?
}
menulist() {
# Args: List of items for menu.
# returns: selected tag
local menuText="$1"
shift
local menuList
for i in "${@}" ; do
menuList+=("$i" "$i")
done
dialog --title "I38" \
--backtitle "Use the arrow keys to find the option you want, and enter to select it." \
--clear \
--no-tags \
--menu "$menuText" 0 0 0 "${menuList[@]}" --stdout
return $?
}
# rangebox
# $1 text to show
# $2 minimum value
# $3 maximum value
# $4 Default value
rangebox() {
dialog --title "I38" \
--backtitle "Use the arrow keys to select a number, then press enter." \
--rangebox "$1" -1 -1 "$2" "$3" "$4" --stdout
}
yesno() {
# Returns: Yes 0 or No 1
# Args: Question to user.
if [[ $defaultConfig -eq 1 ]]; then
return 0
fi
dialog --clear --title "I38" --yesno "$*" -1 -1 --stdout
return $?
}
first_available_program() {
local program
for program in "$@"; do
if command -v "${program#-}" &> /dev/null; then
printf '%s\n' "${program#-}"
return 0
fi
done
return 1
}
first_available_path() {
local program
if program="$(first_available_program "$@")"; then
command -v "$program"
fi
}
set_default_configuration() {
escapeKey="Mod1+Escape"
mod="Mod1"
kbd=()
volumeJump=5
i3FontSize=12
screenReader="$(first_available_path cthulhu orca || true)"
screenReader="${screenReader:-none}"
emailClient="$(first_available_path betterbird evolution thunderbird || true)"
webBrowser="$(first_available_path brave chromium epiphany firefox google-chrome-stable google-chrome-unstable microsoft-edge-stable microsoft-edge-beta microsoft-edge-dev midori seamonkey vivaldi || true)"
textEditor="$(first_available_path emacs geany gedit kate kwrite l3afpad leafpad libreoffice mousepad pluma || true)"
fileBrowser="$(first_available_path caja nemo nautilus pcmanfm pcmanfm-qt thunar || true)"
ircClient="$(first_available_path albikirc Albikirc access-irc || true)"
terminalEmulator="$(first_available_path mate-terminal lxterminal gnome-terminal terminator xfce4-terminal tilix ptyxis kgx sakura roxterm termit guake tilda qterminal konsole || true)"
if [[ -z "$terminalEmulator" ]] && command -v xterm &> /dev/null && command -v fenrir &> /dev/null; then
terminalEmulator="$(command -v xterm)"
fi
udiskie=1
if command -v udiskie &> /dev/null; then
udiskie=0
fi
dex=1
if command -v dex &> /dev/null; then
dex=0
fi
batteryAlert=1
if command -v acpi &> /dev/null; then
batteryAlert=0
fi
brlapi=1
if [[ "$screenReader" != "none" ]] && [[ -n "$screenReader" ]]; then
brlapi=0
fi
sounds=0
screenlockMethod="none"
screenlockPinHash=""
screenlockAutolockSeconds=0
screenlockOnStartup=1
if i38lock_is_supported; then
screenlockMethod="i38lock"
screenlockAutolockSeconds=300
screenlockOnStartup=0
fi
personalModeEnabled=0
personalModeKey="Control+Escape"
waytrayUseI38Config=0
}
# Personal mode helpers
normalize_ratpoison_key() {
local key="$1"
key="${key//Alt/Mod1}"
key="${key//Super Left/Super_L}"
key="${key//Super Right/Super_R}"
echo "$key"
}
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
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"
}
select_screenlock_autolock_seconds() {
local selectedAutolock
if ! selectedAutolock="$(dialog --title "I38" \
--backtitle "Use the arrow keys to choose how long I38 should wait before locking the screen automatically." \
--clear \
--default-item "0" \
--no-tags \
--menu "Autolock screen after:" 0 0 0 \
"0" "Never" \
"180" "3 minutes" \
"300" "5 minutes" \
"600" "10 minutes" \
"1800" "30 minutes" \
"3600" "1 hour" \
--stdout)"; then
selectedAutolock="0"
fi
echo "$selectedAutolock"
}
select_screenlock_method() {
local defaultMethod="$1"
local -a lockOptions=(
"pin" "I38 PIN privacy screen"
"none" "Do not configure screen lock"
)
if i38lock_is_supported; then
lockOptions=(
"i38lock" "I38Lock secure PAM locker"
"${lockOptions[@]}"
)
fi
if [[ "$defaultMethod" != "pin" && "$defaultMethod" != "i38lock" && "$defaultMethod" != "none" ]]; then
if i38lock_is_supported; then
defaultMethod="i38lock"
else
defaultMethod="pin"
fi
fi
dialog --title "I38" \
--backtitle "Use the arrow keys to choose the screen lock I38 should configure." \
--clear \
--default-item "$defaultMethod" \
--menu "Screen lock method:" 0 0 0 \
"${lockOptions[@]}" \
--stdout
}
screen_reader_name() {
if [[ "$screenReader" == "none" || -z "$screenReader" ]]; then
echo "none"
return
fi
echo "${screenReader##*/}"
}
orca_major_version() {
local orcaPath="$1"
local versionLine
versionLine="$("$orcaPath" --version 2>&1 | awk '/^Orca version / { split($3, parts, "."); print parts[1]; exit }')"
if [[ "$versionLine" =~ ^[0-9]+$ ]]; then
echo "$versionLine"
fi
}
orca_is_i38lock_supported() {
local orcaPath="$1"
local majorVersion
if [[ -z "$orcaPath" ]] || ! command -v "$orcaPath" &> /dev/null; then
return 1
fi
majorVersion="$(orca_major_version "$orcaPath")"
[[ "$majorVersion" =~ ^[0-9]+$ ]] && [[ "$majorVersion" -ge 50 ]]
}
i38lock_is_supported() {
local readerName orcaPath
if ! command -v i38lock &> /dev/null; then
return 1
fi
readerName="$(screen_reader_name)"
if [[ "$readerName" == "cthulhu" ]] || command -v cthulhu &> /dev/null; then
return 0
fi
if [[ "$readerName" == "orca" ]]; then
orcaPath="$screenReader"
else
orcaPath="$(command -v orca 2> /dev/null || true)"
fi
orca_is_i38lock_supported "$orcaPath"
}
screenlock_is_configured() {
[[ "$screenlockMethod" == "pin" || "$screenlockMethod" == "i38lock" ]]
}
screenlock_command() {
if [[ "$screenlockMethod" == "i38lock" ]]; then
command -v i38lock
else
printf '%s/scripts/screenlock.sh\n' "$i3Path"
fi
}
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"
}
terminal_command() {
local terminalPath="$1"
local terminalName="${terminalPath##*/}"
if [[ "$terminalName" == "xterm" ]]; then
printf '%s -T "I38 terminal" -e "env ORC_CODE=backup fenrir -x"' "$terminalPath"
else
printf '%s' "$terminalPath"
fi
}
screen_reader_command() {
local screenReaderPath="$1"
if [[ "${screenReaderPath##*/}" == "cthulhu" ]]; then
printf 'env ORC_CODE=backup %s' "$screenReaderPath"
else
printf '%s' "$screenReaderPath"
fi
}
load_config() {
# Load existing configuration if available
if [[ -f "$configFile" ]]; then
# shellcheck source=/dev/null
source "$configFile"
# Convert kbd string back to array if present
if [[ -n "${kbd[*]:-}" ]]; then
IFS=' ' read -ra kbd <<< "$kbd"
fi
return 0
fi
return 1
}
save_config() {
if [[ $defaultConfig -eq 1 ]]; then
return 0
fi
if yesno "Save this configuration for future runs?"; then
cat > "$configFile" << EOF
# I38 Configuration Preferences
# Generated by i38.sh on $(date)
# Edit this file to change saved preferences or delete to reconfigure from scratch
# Keyboard configuration
escapeKey="$escapeKey"
mod="$mod"
# Keyboard layouts (space-separated)
kbd="${kbd[*]}"
# Volume settings
volumeJump="$volumeJump"
i3FontSize="${i3FontSize:-12}"
# Application paths
screenReader="$screenReader"
emailClient="$emailClient"
webBrowser="$webBrowser"
textEditor="$textEditor"
fileBrowser="$fileBrowser"
ircClient="$ircClient"
terminalEmulator="$terminalEmulator"
# Boolean settings (0=yes, 1=no)
udiskie="$udiskie"
dex="$dex"
batteryAlert="${batteryAlert:-1}"
brlapi="$brlapi"
sounds="$sounds"
# Screen lock
screenlockMethod="${screenlockMethod:-none}"
screenlockPinHash="$screenlockPinHash"
screenlockAutolockSeconds="${screenlockAutolockSeconds:-0}"
screenlockOnStartup="${screenlockOnStartup:-1}"
# Personal mode
personalModeEnabled="${personalModeEnabled:-0}"
personalModeKey="$personalModeKey"
# WayTray configuration (0=use I38 config, 1=keep user config)
waytrayUseI38Config="${waytrayUseI38Config:-0}"
EOF
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
}
help() {
echo "${0##*/}"
echo "Released under the terms of the GPL V3 License: https://www.gnu.org/licenses/"
echo -e "This is a Stormux project: https://stormux.org\n"
echo -e "Usage:\n"
echo "With no arguments, create the i3 configuration."
for i in "${!command[@]}" ; do
echo "-${i/:/ <parameter>}: ${command[${i}]}"
done | sort
echo ""
echo "Configuration preferences can be saved to I38_preferences.conf in the current directory."
echo "Delete this file to reconfigure from scratch."
exit 0
}
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
#
# ~/.xinitrc
#
# Executed by startx (run your window manager from here)
[ -f "$HOME/.Xresources" ] && xrdb -merge -I"$HOME" "$HOME/.Xresources"
if [ -d /etc/X11/xinit/xinitrc.d ]; then
for f in /etc/X11/xinit/xinitrc.d/?*.sh; do
[ -r "$f" ] && . "$f"
done
unset f
fi
[ -f /etc/xprofile ] && . /etc/xprofile
[ -f "$HOME/.xprofile" ] && . "$HOME/.xprofile"
EOF
echo "$sessionCommand" >> ~/.xinitrc
chmod +x ~/.xinitrc
}
write_xprofile() {
local xprofilePath="$HOME/.xprofile"
if [[ -f "$xprofilePath" ]]; then
if ! yesno "Would you like to add accessibility variables to your $xprofilePath? Without these, accessibility will be limited or may not work at all. Do you want to continue?"; then
exit 0
fi
fi
I38_XPROFILE_PATH="$xprofilePath" python3 - << 'PY'
import os
from pathlib import Path
xprofile_path = Path(os.environ["I38_XPROFILE_PATH"])
start_marker = "# I38 accessibility variables start"
end_marker = "# I38 accessibility variables end"
block = """\
{start}
export XDG_CURRENT_DESKTOP=i3
export XDG_SESSION_DESKTOP=i3
export ACCESSIBILITY_ENABLED=1
export CHROME_FLAGS="--force-renderer-accessibility"
export GTK_MODULES=gail:atk-bridge
export GNOME_ACCESSIBILITY=1
export QT_ACCESSIBILITY=1
export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1
export SAL_USE_VCLPLUGIN=gtk3
{end}
""".format(start=start_marker, end=end_marker)
content = ""
if xprofile_path.exists():
content = xprofile_path.read_text(encoding="utf-8")
lines = content.splitlines()
output = []
in_i38_block = False
for line in lines:
if line == start_marker:
in_i38_block = True
continue
if line == end_marker:
in_i38_block = False
continue
if not in_i38_block:
output.append(line)
new_content = "\n".join(output).rstrip()
if new_content:
new_content += "\n\n"
new_content += block
xprofile_path.write_text(new_content, encoding="utf-8")
PY
}
apply_screenlock_pin() {
local pinFile="${i3Path}/.screenpin"
local pinValue="$screenlockPinHash"
if [[ "$screenlockMethod" != "pin" ]] || [[ -z "$pinValue" ]]; then
rm -f "$pinFile"
return 0
fi
printf "%s\n" "$pinValue" > "$pinFile"
chmod 600 "$pinFile"
}
copy_scripts() {
mkdir -p "${i3Path}/scripts"
rsync -rv \
--exclude='__pycache__/' \
--exclude='*.pyc' \
--exclude='*.pyo' \
--exclude='*.pyd' \
scripts/ "${i3Path}/scripts/"
find "${i3Path}/scripts" -type f \
\( -name '*.pyc' -o -name '*.pyo' -o -name '*.pyd' \) \
-delete
find "${i3Path}/scripts" -depth -type d -name '__pycache__' \
-exec rm -rf -- {} +
}
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
copy_scripts | dialog --backtitle "I38" --progressbox "Updating scripts..." -1 -1
write_i38_version
write_desktop_shortcuts_template
if [[ -n "$existingPinHash" ]]; then
screenlockPinHash="$existingPinHash"
screenlockMethod="${screenlockMethod:-pin}"
apply_screenlock_pin
fi
exit 0
}
write_desktop_shortcuts_template() {
local shortcutsFile="${i3Path}/desktop_shortcuts"
if [[ -f "$shortcutsFile" ]]; then
return 0
fi
cat << 'EOF' > "$shortcutsFile"
# I38 desktop quick-launch shortcuts
# Format: Pretty Name|/path/to/application --flags-if-any|optional workspace
# The workspace field is optional. Leave it empty to launch on the current workspace.
# Lines starting with # and blank lines are ignored.
#
# Examples:
# Firefox|/usr/bin/firefox|9
# Brave|/usr/bin/brave|
EOF
}
write_i38_xresources() {
local xresourcesFile="${i3Path}/I38.Xresources"
cat << 'EOF' > "$xresourcesFile"
! Visual settings generated by I38
Xcursor.size: 48
EOF
}
write_waytray_config() {
# Only create config if waytray binaries are detected
if ! command -v waytray-daemon &> /dev/null || ! command -v waytray &> /dev/null ; then
return 0
fi
local waytrayConfigDir="${XDG_CONFIG_HOME:-$HOME/.config}/waytray"
local waytrayConfig="${waytrayConfigDir}/config.toml"
mkdir -p "${waytrayConfigDir}"
# Ask user if config already exists (unless preference already saved)
if [[ -f "${waytrayConfig}" ]]; then
if [[ -z "$waytrayUseI38Config" ]]; then
if yesno "Existing waytray configuration detected. Replace with I38's minimal tray-only config?\n\n(Select 'No' to keep your existing waytray configuration with all modules enabled)"; then
waytrayUseI38Config=0
else
waytrayUseI38Config=1
fi
fi
if [[ $waytrayUseI38Config -ne 0 ]]; then
return 0 # User wants to keep existing config
fi
fi
# Create I38's minimal tray-only config
cat << 'EOF' > "${waytrayConfig}"
# WayTray Configuration - Generated by I38
# I38 provides its own battery, weather, and system info utilities
# This config enables ONLY the system tray (SNI) functionality
[modules]
order = ["tray"]
[modules.tray]
enabled = true
[notifications]
enabled = true
timeout_ms = 5000
EOF
return 0
}
i38_current_commit() {
if ! command -v git &> /dev/null; then
return 1
fi
git -C "$PWD" rev-parse --verify HEAD 2> /dev/null
}
write_i38_version() {
mkdir -p "${i3Path}"
if i38Commit="$(i38_current_commit)"; then
printf '%s\n' "$i38Commit" > "$i38VersionFile"
else
rm -f "$i38VersionFile"
fi
}
# Array of command line arguments
declare -A command=(
[h]="This help screen."
[d]="Generate the default I38 configuration without prompting."
[D]="Generate the default I38 configuration plus ~/.xinitrc and ~/.xprofile without prompting."
[u]="Copy over the latest version of scripts."
[x]="Generate ~/.xinitrc and ~/.xprofile."
[X]="Generate ~/.xprofile only."
)
# Convert the keys of the associative array to a format usable by getopts
args="${!command[*]}"
args="${args//[[:space:]]/}"
while getopts "${args}" i ; do
case "$i" in
h) help;;
d) defaultConfig=1;;
D) defaultConfig=1; defaultGenerateX=1;;
u) update_scripts;;
x) write_xinitrc; write_xprofile; exit 0;;
X) write_xprofile; exit 0;;
esac
done
if [[ $defaultConfig -eq 1 ]]; then
set_default_configuration
fi
if [[ $defaultGenerateX -eq 1 ]]; then
write_xinitrc
write_xprofile
fi
# Load saved configuration if available
configLoaded=0
configChanged=0
if [[ $defaultConfig -eq 0 ]] && load_config; then
configLoaded=1
dialog --title "I38" --msgbox "Loaded saved preferences from $configFile\n\nMissing or invalid values will be prompted." 0 0
fi
# Mod1 alt
# Mod4 super
# Mod2 and Mod3 not usually defined.
# Configuration questions
# Ratpoison mode is enabled by default
export i3Mode=0
# Prevent setting ratpoison mode key to the same as default mode key
if [[ -z "$escapeKey" ]] || [[ -z "$mod" ]]; then
while [[ "$escapeKey" == "$mod" ]] || [[ "$escapeKey" =~ ^Super_ && "$mod" == "Mod4" ]] || [[ "$mod" == "Mod4" && "$escapeKey" =~ ^Super_ ]]; do
escapeKey="$(menulist "Ratpoison mode key:" Control+t Control+z Control+Escape Alt+Escape Control+space "Super Left" "Super Right")"
escapeKey="${escapeKey//Alt/Mod1}"
escapeKey="${escapeKey//Super Left/Super_L}"
escapeKey="${escapeKey//Super Right/Super_R}"
mod="$(menulist "I3 mod key, for top level bindings:" Alt Super)"
mod="${mod//Alt/Mod1}"
mod="${mod//Super/Mod4}"
if [ "$escapeKey" == "$mod" ]; then
dialog --title "I38" --msgbox "Ratpoison and mod key cannot be the same key." -1 -1
elif [[ "$escapeKey" =~ ^Super_ && "$mod" == "Mod4" ]]; then
dialog --title "I38" --msgbox "Ratpoison mode key cannot be a Super key when mod key is Super." -1 -1
fi
done
fi
# Multiple keyboard layouts
if [[ $defaultConfig -eq 0 && ${#kbd[@]} -eq 0 ]]; then
if yesno "Do you want to use multiple keyboard layouts?"; then
unset kbd
while : ; do
kbd+=("$(keyboard_menu)") || break
done
fi
fi
# Volume jump
if [[ -z "$volumeJump" ]]; then
volumeJump=$(rangebox "How much should pressing the volume keys change the volume?" 1 15 5)
fi
if [[ -z "$i3FontSize" ]] || [[ ! "$i3FontSize" =~ ^[0-9]+$ ]]; then
configChanged=1
i3FontSize=$(rangebox "How large should I38 window title text be?" 8 24 12)
if [[ ! "$i3FontSize" =~ ^[0-9]+$ ]]; then
i3FontSize=12
fi
fi
# Screen Reader
if [[ $defaultConfig -eq 0 ]] && { [[ -z "$screenReader" ]] || { [[ "$screenReader" != "none" ]] && ! command -v "$screenReader" &> /dev/null; }; }; then
programList=()
for i in cthulhu orca ; do
if command -v "${i/#-/}" &> /dev/null ; then
programList+=("$i")
fi
done
programList+=("None")
if [[ ${#programList[@]} -gt 1 ]]; then
screenReader="$(menulist ":Screen Reader" "${programList[@]}")"
else
screenReader="${programList[0]#-}"
fi
if [[ "$screenReader" == "None" ]]; then
screenReader="none"
else
screenReader="$(command -v "$screenReader")"
fi
export screenReader
else
# Validate and export existing preference
export screenReader
fi
# Email client
if [[ $defaultConfig -eq 0 ]] && { [[ -z "$emailClient" ]] || ! command -v "$emailClient" &> /dev/null; }; then
programList=()
for i in betterbird evolution thunderbird ; do
if command -v "${i/#-/}" &> /dev/null ; then
programList+=("$i")
fi
done
if [[ ${#programList[@]} -gt 1 ]]; then
emailClient="$(menulist "Email client:" "${programList[@]}")"
else
emailClient="${programList[0]#-}"
fi
emailClient="$(command -v "$emailClient")"
export emailClient
else
# Validate and export existing preference
export emailClient
fi
# Web browser
if [[ $defaultConfig -eq 0 ]] && { [[ -z "$webBrowser" ]] || ! command -v "$webBrowser" &> /dev/null; }; then
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
programList+=("$i")
fi
done
if [[ ${#programList[@]} -gt 1 ]]; then
webBrowser="$(menulist "Web browser:" "${programList[@]}")"
else
webBrowser="${programList[0]#-}"
fi
webBrowser="$(command -v "$webBrowser")"
export webBrowser
else
# Validate and export existing preference
export webBrowser
fi
# Text editor
if [[ $defaultConfig -eq 0 ]] && { [[ -z "$textEditor" ]] || ! command -v "$textEditor" &> /dev/null; }; then
programList=()
for i in emacs geany gedit kate kwrite l3afpad leafpad libreoffice mousepad pluma ; do
if command -v "${i/#-/}" &> /dev/null ; then
programList+=("$i")
fi
done
if [[ ${#programList[@]} -gt 1 ]]; then
textEditor="$(menulist "Text editor:" "${programList[@]}")"
else
textEditor="${programList[0]#-}"
fi
textEditor="$(command -v "$textEditor")"
export textEditor
else
# Validate and export existing preference
export textEditor
fi
# File browser
if [[ $defaultConfig -eq 0 ]] && { [[ -z "$fileBrowser" ]] || ! command -v "$fileBrowser" &> /dev/null; }; then
programList=()
for i in caja nemo nautilus pcmanfm pcmanfm-qt thunar ; do
if command -v "${i/#-/}" &> /dev/null ; then
programList+=("$i")
fi
done
if [[ ${#programList[@]} -gt 1 ]]; then
fileBrowser="$(menulist "File browser:" "${programList[@]}")"
else
fileBrowser="${programList[0]#-}"
fi
fileBrowser="$(command -v "$fileBrowser")"
export fileBrowser
else
# Validate and export existing preference
export fileBrowser
fi
# IRC client
if [[ $defaultConfig -eq 0 ]] && { [[ -z "$ircClient" ]] || ! command -v "$ircClient" &> /dev/null; }; then
programList=()
for i in albikirc Albikirc access-irc ; do
if command -v "${i/#-/}" &> /dev/null ; then
programList+=("$i")
fi
done
if [[ ${#programList[@]} -gt 1 ]]; then
ircClient="$(menulist "IRC client:" "${programList[@]}")"
else
ircClient="${programList[0]#-}"
fi
ircClient="$(command -v "$ircClient")"
export ircClient
else
# Validate and export existing preference
export ircClient
fi
# Terminal emulator
if [[ $defaultConfig -eq 1 && -z "$terminalEmulator" ]]; then
echo "No supported terminal emulator was found. Please install mate-terminal, lxterminal, gnome-terminal, terminator, another supported accessible terminal, or install both xterm and fenrir."
exit 1
fi
if [[ $defaultConfig -eq 0 ]] && { [[ -z "$terminalEmulator" ]] || { [[ ! -x "$terminalEmulator" ]] && ! command -v "$terminalEmulator" &> /dev/null; }; }; then
configChanged=1
programList=()
for i in mate-terminal lxterminal gnome-terminal terminator xfce4-terminal tilix ptyxis kgx sakura roxterm termit guake tilda qterminal konsole ; do
if command -v "$i" &> /dev/null ; then
programList+=("$i")
fi
done
if command -v xterm &> /dev/null && command -v fenrir &> /dev/null ; then
programList+=("xterm")
fi
if [[ ${#programList[@]} -eq 0 ]]; then
dialog --title "I38" --msgbox "No supported terminal emulator was found. Please install mate-terminal, lxterminal, gnome-terminal, terminator, another supported accessible terminal, or install both xterm and fenrir." -1 -1
exit 1
elif [[ ${#programList[@]} -gt 1 ]]; then
terminalEmulator="$(menulist "Terminal emulator:" "${programList[@]}")"
else
terminalEmulator="${programList[0]}"
fi
terminalEmulator="$(command -v "$terminalEmulator")"
export terminalEmulator
else
# Validate and export existing preference
export terminalEmulator
fi
# Auto mount removable media
if [[ -z "$udiskie" ]]; then
udiskie=1
if command -v udiskie &> /dev/null ; then
if yesno "Would you like removable drives to automatically mount when plugged in?"; then
export udiskie=0
else
export udiskie=1
fi
fi
fi
# Auto start with dex
if [[ -z "$dex" ]]; then
dex=1
if command -v dex &> /dev/null ; then
if yesno "Would you like to autostart applications with dex?"; then
export dex=0
else
export dex=1
fi
fi
fi
if [[ $dex -eq 0 ]] && [[ "$screenReader" != "none" ]] && [[ -n "$screenReader" ]]; then
dex -t "${XDG_CONFIG_HOME:-${HOME}/.config}/autostart" -c "$(screen_reader_command "$screenReader")"
fi
if [[ -z "$batteryAlert" ]]; then
if command -v acpi &> /dev/null ; then
if yesno "Do you want low battery notifications?"; then
batteryAlert=0
else
batteryAlert=1
fi
fi
fi
if [[ "$screenReader" == "none" ]] || [[ -z "$screenReader" ]]; then
brlapi=1
elif [[ -z "$brlapi" ]]; then
if yesno "Do you want to use a braille display with ${screenReader##*/}?"; then
brlapi=0
else
brlapi=1
fi
fi
if [[ -z "$sounds" ]]; then
if yesno "Do you want window event sounds?"; then
sounds=0
else
sounds=1
fi
fi
if [[ $defaultConfig -eq 0 && -z "$screenlockPinHash" ]]; then
screenlockPinFile="${i3Path}/.screenpin"
if [[ -f "$screenlockPinFile" ]]; then
read -r screenlockPinHash < "$screenlockPinFile"
fi
fi
screenlockNeedsSelection=0
if [[ -z "${screenlockMethod+x}" ]]; then
screenlockNeedsSelection=1
fi
if [[ -z "$screenlockMethod" || "$screenlockMethod" == "none" ]]; then
if [[ $defaultConfig -eq 0 && -n "$screenlockPinHash" ]]; then
screenlockMethod="pin"
else
screenlockMethod="none"
fi
fi
if [[ "$screenlockMethod" != "pin" && "$screenlockMethod" != "i38lock" && "$screenlockMethod" != "none" ]]; then
screenlockMethod="none"
fi
if [[ "$screenlockMethod" == "i38lock" ]] && ! i38lock_is_supported; then
dialog --title "I38" --msgbox "I38Lock is not available with the current screen reader setup. I38Lock requires the i38lock command and either Cthulhu or Orca 50 or newer. Please choose another screen lock method." -1 -1
screenlockMethod="none"
screenlockNeedsSelection=1
configChanged=1
fi
if [[ $screenlockNeedsSelection -eq 1 ]]; then
selectedScreenlockMethod="$(select_screenlock_method "$screenlockMethod")"
dialogResult=$?
if [[ $dialogResult -eq 0 ]]; then
screenlockMethod="$selectedScreenlockMethod"
configChanged=1
fi
fi
if [[ "$screenlockMethod" == "pin" ]]; then
if [[ -z "$screenlockPinHash" ]]; 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=""
screenlockMethod="none"
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
else
screenlockPinHash=""
fi
if screenlock_is_configured; then
if [[ -z "${screenlockOnStartup+x}" ]]; then
configChanged=1
if yesno "Lock the screen automatically when I38 starts? If your screen reader or display setup fails, you may need to unlock without feedback."; then
screenlockOnStartup=0
else
screenlockOnStartup=1
fi
fi
if command -v xprintidle &> /dev/null; then
if [[ -z "${screenlockAutolockSeconds+x}" ]] || [[ ! "$screenlockAutolockSeconds" =~ ^[0-9]+$ ]]; then
configChanged=1
screenlockAutolockSeconds="$(select_screenlock_autolock_seconds)"
fi
elif [[ -z "${screenlockAutolockSeconds+x}" ]] || [[ ! "$screenlockAutolockSeconds" =~ ^[0-9]+$ ]]; then
screenlockAutolockSeconds=0
fi
else
screenlockAutolockSeconds=0
screenlockOnStartup=1
fi
# Personal mode
personalModeExists=1
if personal_mode_exists; then
personalModeExists=0
personalModeEnabled=0
fi
if [[ $personalModeExists -ne 0 ]]; then
personalModeEnabled=0
if [[ -z "$personalModeKey" ]] || [[ "$personalModeKey" == "$escapeKey" ]]; then
personalModeKey="$(select_personal_mode_key)"
fi
if [[ -z "$personalModeKey" ]]; then
personalModeEnabled=1
fi
fi
# Save configuration if requested on first run or after filling missing saved values.
if [[ $configLoaded -eq 0 || $configChanged -eq 1 ]]; then
save_config
fi
terminalExec="$(terminal_command "$terminalEmulator")"
screenReaderExec="$(screen_reader_command "$screenReader")"
if [[ -d "${i3Path}" ]]; then
yesno "This will replace your existing configuration at ${i3Path}. Do you want to continue?" || exit 0
fi
# Configure waytray if available
write_waytray_config
# Create the i3 configuration directory.
mkdir -p "${i3Path}"
write_i38_version
write_desktop_shortcuts_template
write_i38_xresources
# Move scripts into place
if [[ $defaultConfig -eq 1 ]]; then
copy_scripts > /dev/null
else
copy_scripts | dialog --backtitle "I38" --progressbox "Moving scripts into place and writing config..." -1 -1
fi
apply_screenlock_pin
update_personal_customizations
cat << EOF > "${i3Path}/config"
# Generated by I38 (${0##*/}) https://git.stormux.org/storm/I38
# $(date '+%A, %B %d, %Y at %I:%M%p')
EOF
cat << EOF >> "${i3Path}/config"
# i3 config file (v4)
#
# Please see https://i3wm.org/docs/userguide.html for a complete reference!
#
# This config file uses keycodes (bindsym) and was written for the QWERTY
# layout.
# set mod key
set \$mod $mod
# set workspace layout to tabbed so apps use most of the screen
workspace_layout tabbed
# set the mouse so it is trapped in focused window
# this fixes some issues in some games that require focus and pause when focus is moved via mouse accidentally
focus_follows_mouse no
# Font for window titles and tab labels.
font pango:monospace ${i3FontSize}
# Window rules
# Fix Wine window issues - improves stability and prevents lockups
for_window [class="Wine"] floating enable
# I38 help - Open comprehensive HTML guide
bindsym \$mod+Shift+F1 exec $webBrowser ${i3Path}/I38.html
# Run dialog
bindsym \$mod+F2 exec ${i3Path}/scripts/run_dialog.py
# Bookmarks dialog
bindsym \$mod+Control+b exec ${i3Path}/scripts/bookmarks.sh
# Clipboard manager
bindsym \$mod+Control+c exec ${i3Path}/scripts/i38-clipboard.py --show
# Move the mouse pointer to the center of the focused window
bindsym \$mod+Control+m exec --no-startup-id ${i3Path}/scripts/find_mouse.sh
$(if command -v xzoom &> /dev/null; then
echo "# Magnify part of the screen with xzoom"
echo "bindsym \$mod+Control+z exec --no-startup-id xzoom"
echo
fi)
# gtk bar
bindsym \$mod+Control+Delete exec --no-startup-id sgtk-bar
# Use pactl to adjust volume in PulseAudio.
# Increase system volume
bindsym \$mod+XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ +${volumeJump}% & play -qnG synth 0.03 sin 440
# Decrease system volume
bindsym \$mod+XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -${volumeJump}% & play -qnG synth 0.03 sin 440
# Mute/unmute system volume
bindsym \$mod+XF86AudioMute exec --no-startup-id ${i3Path}/scripts/mute-unmute.sh
# Music player controls
# Increase music volume
bindsym XF86AudioRaiseVolume exec --no-startup-id ${i3Path}/scripts/music_controler.sh incvol $volumeJump
# Decrease music volume
bindsym XF86AudioLowerVolume exec --no-startup-id ${i3Path}/scripts/music_controler.sh decvol $volumeJump
# Previous track
bindsym XF86AudioPrev exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh prev
# Pause music playback
bindsym XF86AudioMute exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh pause
# Play music
bindsym XF86AudioPlay exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh play
# Get music player information
bindsym \$mod+XF86AudioPlay exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh info
# Stop music playback
bindsym XF86AudioStop exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh stop
# Next track
bindsym XF86AudioNext exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh next
# start a terminal
bindsym \$mod+Return exec $terminalExec
# Close focused window
bindsym \$mod+F4 kill
# Applications menu
bindsym \$mod+F1 exec --no-startup-id "${i3Path}/scripts/menu.py"
# Desktop icons
bindsym \$mod+Control+d exec --no-startup-id ${i3Path}/scripts/desktop.sh
# change focus
# Focus previous window (alt+shift+tab)
bindsym Mod1+Shift+Tab focus left
# Focus next window (alt+tab)
bindsym Mod1+Tab focus right
# Toggle fullscreen for the focused window
bindsym \$mod+BackSpace fullscreen toggle
# Move the currently focused window to the scratchpad
bindsym \$mod+Shift+minus move scratchpad
# Bind the focused window to the scratchpad so it always opens there
bindsym \$mod+Shift+equal exec --no-startup-id ${i3Path}/scripts/bind_to_scratchpad.sh
# Show or cycle scratchpad windows
bindsym \$mod+minus scratchpad show
# Define names for default workspaces for which we configure key bindings later on.
# We use variables to avoid repeating the names in multiple places.
set \$ws1 "1"
set \$ws2 "2"
set \$ws3 "3"
set \$ws4 "4"
set \$ws5 "5"
set \$ws6 "6"
set \$ws7 "7"
set \$ws8 "8"
set \$ws9 "9"
set \$ws10 "10"
# switch to workspace
# Switch to workspace 1
bindsym Control+F1 workspace number \$ws1, exec --no-startup-id ${i3Path}/scripts/announce_workspace.sh
# Switch to workspace 2
bindsym Control+F2 workspace number \$ws2, exec --no-startup-id ${i3Path}/scripts/announce_workspace.sh
# Switch to workspace 3
bindsym Control+F3 workspace number \$ws3, exec --no-startup-id ${i3Path}/scripts/announce_workspace.sh
# Switch to workspace 4
bindsym Control+F4 workspace number \$ws4, exec --no-startup-id ${i3Path}/scripts/announce_workspace.sh
# Switch to workspace 5
bindsym Control+F5 workspace number \$ws5, exec --no-startup-id ${i3Path}/scripts/announce_workspace.sh
# Switch to workspace 6
bindsym Control+F6 workspace number \$ws6, exec --no-startup-id ${i3Path}/scripts/announce_workspace.sh
# Switch to workspace 7
bindsym Control+F7 workspace number \$ws7, exec --no-startup-id ${i3Path}/scripts/announce_workspace.sh
# Switch to workspace 8
bindsym Control+F8 workspace number \$ws8, exec --no-startup-id ${i3Path}/scripts/announce_workspace.sh
# Switch to workspace 9
bindsym Control+F9 workspace number \$ws9, exec --no-startup-id ${i3Path}/scripts/announce_workspace.sh
# Switch to workspace 10
bindsym Control+F10 workspace number \$ws10, exec --no-startup-id ${i3Path}/scripts/announce_workspace.sh
# move focused container to workspace
# Move window to workspace 1
bindsym Control+Shift+F1 move container to workspace number \$ws1, exec spd-say -P important -Cw "moved to workspace 1"
# Move window to workspace 2
bindsym Control+Shift+F2 move container to workspace number \$ws2, exec spd-say -P important -Cw "moved to workspace 2"
# Move window to workspace 3
bindsym Control+Shift+F3 move container to workspace number \$ws3, exec spd-say -P important -Cw "moved to workspace 3"
# Move window to workspace 4
bindsym Control+Shift+F4 move container to workspace number \$ws4, exec spd-say -P important -Cw "moved to workspace 4"
# Move window to workspace 5
bindsym Control+Shift+F5 move container to workspace number \$ws5, exec spd-say -P important -Cw "moved to workspace 5"
# Move window to workspace 6
bindsym Control+Shift+F6 move container to workspace number \$ws6, exec spd-say -P important -Cw "moved to workspace 6"
# Move window to workspace 7
bindsym Control+Shift+F7 move container to workspace number \$ws7, exec spd-say -P important -Cw "moved to workspace 7"
# Move window to workspace 8
bindsym Control+Shift+F8 move container to workspace number \$ws8, exec spd-say -P important -Cw "moved to workspace 8"
# Move window to workspace 9
bindsym Control+Shift+F9 move container to workspace number \$ws9, exec spd-say -P important -Cw "moved to workspace 9"
# Move window to workspace 10
bindsym Control+Shift+F10 move container to workspace number \$ws10, exec spd-say -P important -Cw "moved to workspace 10"
# A mode that will pass all keys except $mod+shift+backspace to the current application.
# Enter bypass mode
bindsym $mod+shift+BackSpace mode "bypass"
mode "bypass" {
# Exit bypass mode.
bindsym $mod+Shift+BackSpace mode "default"
}
EOF
if [[ "$screenlockMethod" == "pin" ]]; 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"
# Interrupt speech output
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"
fi
# Create panel mode
cat << EOF >> "${i3Path}/config"
# Enter Panel Mode
bindsym Control+Mod1+Tab mode "panel"
mode "panel" {
# Panel mode keybindings help bound to F1
bindsym F1 exec ${i3Path}/scripts/i38-help-panel.sh, mode "default"
# Weather information bound to w
bindsym w exec --no-startup-id ${i3Path}/scripts/weather.sh, mode "default"
# Magic wormhole bound to shift+W
bindsym Shift+w exec --no-startup-id ${i3Path}/scripts/wormhole.py, mode "default"
$(if command -v waytray &> /dev/null ; then
echo " # System tray bound to t"
echo " bindsym t exec --no-startup-id waytray, mode \"default\""
echo " "
fi)
# System information bound to s
bindsym s exec --no-startup-id ${i3Path}/scripts/sysinfo.sh, mode "default"
$(if command -v remind &> /dev/null ; then
echo "# Reminders bound to r"
echo "bindsym r exec --no-startup-id ${i3Path}/scripts/reminder.sh, mode \"default\""
fi)
# Simple notes system bound to n
bindsym n exec --no-startup-id ${i3Path}/scripts/notes.py, mode "default"
# Password manager bound to m
bindsym m exec --no-startup-id ${i3Path}/scripts/passmanager.py, mode "default"
$(if command -v blueman-manager &> /dev/null ; then
echo "# Bluetooth bound to b"
echo "bindsym b exec --no-startup-id blueman-manager, mode \"default\""
fi)
# Quick battery status bound to the configured mod key+b
bindsym \$mod+b exec --no-startup-id ${i3Path}/scripts/battery_status.sh, mode "default"
# Detailed battery information bound to Shift+b
bindsym Shift+b exec --no-startup-id ${i3Path}/scripts/battery_status.sh --detailed, mode "default"
# Power options bound to p
bindsym p exec --no-startup-id ${i3Path}/scripts/power.sh, mode "default"
$(if screenlock_is_configured; then
if [[ "$screenlockMethod" == "i38lock" ]]; then
echo " # Screen lock (I38Lock secure PAM locker)"
else
echo " # Screen lock (I38 PIN privacy screen)"
fi
echo " bindsym Control+\$mod+l exec --no-startup-id $(screenlock_command), mode \"default\""
echo " "
fi)
# Exit panel mode without any action
bindsym Escape mode "default"
bindsym Control+g mode "default"
}
EOF
# Create ratpoison mode if requested.
if [[ -n "${escapeKey}" ]]; then
cat << EOF >> "${i3Path}/config"
# Enter ratpoison mode
bindsym $escapeKey mode "ratpoison"
mode "ratpoison" {
# Ratpoison mode keybindings help bound to F1
bindsym F1 exec ${i3Path}/scripts/i38-help-rp.sh, mode "default"
# Terminal emulator bound to c
bindsym c exec $terminalExec, mode "default"
# Text editor bound to e
bindsym e exec $textEditor, mode "default"
$(if [[ ${#fileBrowser} -gt 3 ]]; then
echo "# File browser bound to f"
echo "bindsym f exec $fileBrowser, mode \"default\""
fi)
# Email client bound to \$mod+e
bindsym \$mod+e exec $emailClient, mode "default"
# Web browser bound to w
bindsym w exec $webBrowser, mode "default"
$(if command -v steam &> /dev/null ; then
echo "# Steam bound to s"
echo "bindsym s exec --no-startup-id command steam -bigpicture, mode \"default\""
fi)
# Kill window bound to k
bindsym k kill, mode "default"
$(if command -v mumble &> /dev/null ; then
echo "# Mumble bound to m"
echo "bindsym m exec $(command -v mumble), mode \"default\""
fi)
$(if command -v ocrdesktop &> /dev/null ; then
echo "# OCR desktop bound to print screen alternative \$mod+r"
echo "bindsym Print exec $(command -v ocrdesktop) -b, mode \"default\""
echo "bindsym \$mod+r exec $(command -v ocrdesktop) -b, mode \"default\""
fi)
$(if command -v pidgin &> /dev/null ; then
echo "# p bound to pidgin"
echo "bindsym p exec $(command -v pidgin), mode \"default\""
fi)
$(if [[ ${#ircClient} -gt 3 ]]; then
echo "# IRC client bound to i"
echo "bindsym i exec $ircClient, mode \"default\""
fi)
$(if command -v xrandr &> /dev/null ; then
echo "# alt+s bound to brightness control"
echo "bindsym \$mod+s exec --no-startup-id ${i3Path}/scripts/screen_controller.sh, mode \"default\""
fi)
#Keyboard based volume Controls with pulseaudio
bindsym Mod1+Shift+0 exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ +${volumeJump}% & play -qnG synth 0.03 sin 440
bindsym Mod1+Shift+9 exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -${volumeJump}% & play -qnG synth 0.03 sin 440
# Music player controls
# Requires playerctl.
bindsym Mod1+Shift+equal exec --no-startup-id ${i3Path}/scripts/music_controler.sh incvol $volumeJump, mode "default"
bindsym Mod1+Shift+minus exec --no-startup-id ${i3Path}/scripts/music_controler.sh decvol $volumeJump, mode "default"
bindsym Mod1+Shift+z exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh prev, mode "default"
bindsym Mod1+Shift+c exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh pause, mode "default"
bindsym Mod1+Shift+x exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh play, mode "default"
bindsym Mod1+Shift+v exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh stop, mode "default"
bindsym Mod1+Shift+b exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh next, mode "default"
bindsym Mod1+Shift+u exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh info, mode "default"
#Check battery status
bindsym Mod1+b exec --no-startup-id ${i3Path}/scripts/battery_status.sh, mode "default"
#Check controller battery status
bindsym g exec ${i3Path}/scripts/game_controller.sh -s, mode "default"
# AI Assistant bound to a
bindsym a exec ${i3Path}/scripts/ai.py, mode "default"
# Toggle desktop audio recording and slideshow video conversion
bindsym Mod1+Shift+r exec --no-startup-id ${i3Path}/scripts/slideshow_record_toggle.sh, mode "default"
# Get a list of windows in the current workspace
bindsym apostrophe exec --no-startup-id ${i3Path}/scripts/window_list.sh, mode "default"
$(if command -v cthulhu &> /dev/null; then
echo "# Restart Cthulhu"
echo "bindsym Shift+c exec env ORC_CODE=backup $(command -v cthulhu) --replace, mode \"default\""
fi)
$(if command -v orca &> /dev/null; then
echo "# Restart Orca"
echo "bindsym Shift+o exec $(command -v orca) --replace, mode \"default\""
fi)
$(if [[ "$screenReader" != "none" ]] && [[ -n "$screenReader" ]]; then
echo "# Toggle screen reader"
echo "bindsym Shift+t exec ${i3Path}/scripts/toggle_screenreader.sh, mode \"default\""
fi)
# reload the configuration file
bindsym Control+semicolon exec bash -c '$i3msg -t run_command reload && spd-say -P important -Cw "I38 Configuration reloaded."', mode "default"
# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)
bindsym Control+Shift+semicolon exec $i3msg -t run_command restart && spd-say -P important -Cw "I3 restarted.", mode "default"
# Run dialog with exclamation
bindsym Shift+exclam exec ${i3Path}/scripts/run_dialog.py, mode "default"
# exit i3 (logs you out of your X session)
bindsym \$mod+q exec bash -c 'yad --image "dialog-question" --title "I38" --button=yes:0 --button=no:1 --text "You pressed the exit shortcut. Do you really want to exit i3? This will end your X session." && $i3msg -t run_command exit'
bindsym Control+\$mod+q exec bash -c 'yad --image "dialog-question" --title "I38" --button=yes:0 --button=no:1 --text "You pressed the exit shortcut. Do you really want to exit i3? This will end your X session." && $i3msg -t run_command exit'
# Exit ratpoison mode without any action escape or Control+g
bindsym Escape mode "default"
bindsym Control+g mode "default"
}
EOF
fi
cat << EOF >> "${i3Path}/config"
# Auto start section
$(if [[ $sounds -eq 0 ]]; then
echo "exec_always --no-startup-id ${i3Path}/scripts/sound.py"
fi
if command -v xrdb &> /dev/null; then
echo "exec --no-startup-id xrdb -merge ${i3Path}/I38.Xresources"
fi
# Steam game focus handler - focuses games when they open behind Big Picture
if command -v steam &> /dev/null; then
echo "exec --no-startup-id ${i3Path}/scripts/steam_games.py"
fi
# i3 watchdog - monitors i3 responsiveness and auto-recovers from freezes
echo "exec_always --no-startup-id ${i3Path}/scripts/i3_watchdog.sh"
if [[ $brlapi -eq 0 ]]; then
echo 'exec --no-startup-id xbrlapi --quiet'
fi
if [[ $udiskie -eq 0 ]]; then
echo 'exec --no-startup-id udiskie'
fi
# Notification daemon
if [[ -x "/usr/lib/xfce4/notifyd/xfce4-notifyd" ]]; then
echo 'exec_always --no-startup-id /usr/lib/xfce4/notifyd/xfce4-notifyd'
elif [[ -x "/usr/lib/notification-daemon-1.0/notification-daemon" ]]; then
echo 'exec_always --no-startup-id /usr/lib/notification-daemon-1.0/notification-daemon -r'
elif [[ -x "/usr/libexec/notification-daemon" ]]; then
# Work around for weird Void Linux stuff
echo 'exec_always --no-startup-id /usr/libexec/notification-daemon -r'
fi
if command -v remind &> /dev/null && command -v notify-send &> /dev/null ; then
echo "exec_always --no-startup-id ${i3Path}/scripts/launch_remind.sh"
touch ~/.reminders
fi
if [[ $batteryAlert -eq 0 ]]; then
echo "exec_always --no-startup-id ${i3Path}/scripts/battery_alert.sh"
fi
if screenlock_is_configured && [[ "${screenlockAutolockSeconds:-0}" -gt 0 ]] && command -v xprintidle &> /dev/null; then
if [[ "$screenlockMethod" == "i38lock" ]]; then
echo "exec_always --no-startup-id ${i3Path}/scripts/screenlock_autolock.sh ${screenlockAutolockSeconds} $(screenlock_command) --nofork"
else
echo "exec_always --no-startup-id ${i3Path}/scripts/screenlock_autolock.sh ${screenlockAutolockSeconds} $(screenlock_command)"
fi
fi
if screenlock_is_configured && [[ "${screenlockOnStartup:-1}" -eq 0 ]]; then
echo "exec --no-startup-id bash -c 'sleep 2; $(screenlock_command)'"
fi
# WayTray system tray daemon
if command -v waytray-daemon &> /dev/null ; then
echo 'exec_always --no-startup-id bash -c "pgrep -x waytray-daemon > /dev/null || waytray-daemon"'
fi
case "${fileBrowser##*/}" in
thunar|pcmanfm|pcmanfm-qt)
echo "exec_always --no-startup-id ${i3Path}/scripts/filemanager.sh ${fileBrowser}"
;;
esac
echo "exec_always --no-startup-id ${i3Path}/scripts/i38-clipboard.py --daemon"
echo "exec_always --no-startup-id ${i3Path}/scripts/desktop.sh"
echo "exec_always --no-startup-id ${i3Path}/scripts/i38-update-check.sh"
if [[ $dex -eq 0 ]]; then
echo '# Start XDG autostart .desktop files using dex. See also'
echo '# https://wiki.archlinux.org/index.php/XDG_Autostart'
echo 'exec --no-startup-id dex --autostart --environment i3'
else
echo '# Startup applications'
if command -v x11bell &> /dev/null; then
echo 'exec --no-startup-id x11bell play -nqV0 synth .1 sq norm -12'
fi
if [[ "$screenReader" != "none" ]] && [[ -n "$screenReader" ]]; then
echo "exec $screenReaderExec"
fi
fi)
# First run help documentation
exec --no-startup-id bash -c 'if [[ -f "${i3Path}/firstrun" ]]; then ${webBrowser} "${i3Path}/I38.html" & sleep 1 && rm "${i3Path}/firstrun"; fi'
# If you want to add personal customizations to i3, add them in ${i3Path}/customizations
# It is not overwritten when the config file is recreated.
include "${i3Path}/customizations"
EOF
touch "${i3Path}/customizations"
touch "${i3Path}/scratchpad"
# Check for markdown or pandoc for converting the welcome document
if command -v pandoc &> /dev/null ; then
pandoc -f markdown-citations -t html "I38.md" -so "${i3Path}/I38.html" --metadata pagetitle="Welcome to I38"
elif command -v markdown &> /dev/null ; then
cat << EOF > "${i3Path}/I38.html"
<!DOCTYPE html>
<html>
<head>
<title>Welcome to I38</title>
<meta charset="utf-8">
</head>
<body>
EOF
# Convert markdown to html and append to the file
markdown "I38.md" >> "${i3Path}/I38.html"
# Close the HTML tags using heredoc
cat << EOF >> "${i3Path}/I38.html"
</body>
</html>
EOF
fi
# More readable version of variables.
escapeKey="${escapeKey//Mod1/Alt}"
escapeKey="${escapeKey//Super_L/Super Left}"
escapeKey="${escapeKey//Super_R/Super Right}"
mod="${mod//Mod1/Alt}"
mod="${mod//Mod4/Super}"
webBrowser="${webBrowser##*/}"
screenReader="${screenReader##*/}"
textEditor="${textEditor##*/}"
fileBrowser="${fileBrowser##*/}"
ircClient="${ircClient##*/}"
webBrowserHelp="${webBrowser:-no web browser was configured}"
if [[ "$screenReader" == "none" ]] || [[ -z "$screenReader" ]]; then
screenReaderHelp="no screen reader was configured"
else
screenReaderHelp="$screenReader"
fi
textEditorHelp="${textEditor:-no text editor was configured}"
if [[ -n "$fileBrowser" ]]; then
fileBrowserHelp="I38 uses ${fileBrowser} for file management. Launch it in Ratpoison mode with the <code>f</code> key."
else
fileBrowserHelp="No file manager was configured when this help file was generated."
fi
personalModeKeyHelp=""
personalModeKeyForHelp=""
if [[ "${personalModeEnabled:-1}" -eq 0 && -n "$personalModeKey" ]]; then
personalModeKeyForHelp="$personalModeKey"
fi
if [[ -z "$personalModeKeyForHelp" ]] && [[ -f "${i3Path}/customizations" ]]; then
personalModeKeyForHelp="$(awk '/^[[:space:]]*bindsym[[:space:]].*[[:space:]]mode "personal"/ { print $2; exit }' "${i3Path}/customizations")"
fi
if [[ -n "$personalModeKeyForHelp" ]]; then
personalModeKeyDisplay="${personalModeKeyForHelp//Mod1/Alt}"
personalModeKeyDisplay="${personalModeKeyDisplay//Mod4/Super}"
personalModeKeyDisplay="${personalModeKeyDisplay//Super_L/Super Left}"
personalModeKeyDisplay="${personalModeKeyDisplay//Super_R/Super Right}"
personalModeKeyHelp="<code>${personalModeKeyDisplay}</code>: Enter Personal Mode.<br>"
fi
# Extract and categorize top-level keybindings for documentation
# Generate HTML directly since we're inserting into an already-converted HTML file
# Extract comment+bindsym pairs from config (excluding mode blocks)
declare -a applications workspaceSwitch workspaceMove windowMgmt utilities modes media
# Read the config and extract top-level bindings with their comments
while IFS= read -r line; do
if [[ "$line" =~ ^#.*$ ]]; then
# This is a comment line - save it
lastComment="${line#\# }"
elif [[ "$line" =~ ^bindsym.*$ ]]; then
# This is a bindsym line - pair it with the last comment
keybinding="${line#bindsym }"
keybinding="${keybinding//\\$/}"
# Extract just the key combination, which is the first word after bindsym.
keyCombo="${keybinding%% *}"
# Clean up key combo
keyCombo="${keyCombo//\$mod/$mod}"
keyCombo="${keyCombo//Mod1/Alt}"
keyCombo="${keyCombo//Mod4/Super}"
keyCombo="${keyCombo//+shift+/+Shift+}"
keyCombo="${keyCombo//apostrophe/apostrophe}"
keyCombo="${keyCombo//semicolon/semicolon}"
keyCombo="${keyCombo//exclam/exclamation mark}"
keyCombo="${keyCombo//minus/dash}"
keyCombo="${keyCombo//equal/equals sign}"
# Categorize based on the comment or the action
if [[ "$keybinding" == Control+F*workspace*number* ]]; then
# Switch to workspace
workspaceSwitch+=("$keyCombo|$lastComment")
elif [[ "$keybinding" == Control+Shift+F*move*container*workspace* ]]; then
# Move to workspace
workspaceMove+=("$keyCombo|$lastComment")
elif [[ "$lastComment" =~ (menu|terminal|editor|browser|Run dialog) ]] || [[ "$keybinding" =~ (menu.py|run_dialog) ]]; then
applications+=("$keyCombo|$lastComment")
elif [[ "$lastComment" =~ (focus|window|fullscreen|scratchpad|kill|close) ]] || [[ "$keybinding" =~ (focus|kill|fullscreen|scratchpad) ]]; then
windowMgmt+=("$keyCombo|$lastComment")
elif [[ "$lastComment" =~ (mode|ratpoison|panel|bypass) ]] || [[ "$keybinding" =~ mode ]]; then
modes+=("$keyCombo|$lastComment")
elif [[ "$keybinding" =~ (Audio|music_controler|pactl) ]]; then
media+=("$keyCombo|$lastComment")
else
utilities+=("$keyCombo|$lastComment")
fi
lastComment=""
fi
done < <(
awk '
/^mode "/ {
inMode = 1
previousComment = ""
next
}
inMode && /^}$/ {
inMode = 0
next
}
inMode {
next
}
/^#/ {
previousComment = $0
next
}
/^bindsym/ {
if (previousComment != "") {
print previousComment
previousComment = ""
}
print
next
}
{
previousComment = ""
}
' "${i3Path}/config"
)
# Build HTML reference sections without tables, so generated help stays easy to
# navigate with basic screen-reader commands.
emit_keybinding_group() {
local groupTitle="$1"
shift
local item key desc
local -a rows=()
if [[ $# -eq 0 ]]; then
return
fi
for item in "$@"; do
IFS='|' read -r key desc <<< "$item"
if [[ -n "$desc" ]]; then
rows+=("$key|$desc")
fi
done
if [[ ${#rows[@]} -eq 0 ]]; then
return
fi
printf '<p><strong>%s:</strong><br>\n' "$groupTitle"
for item in "${rows[@]}"; do
IFS='|' read -r key desc <<< "$item"
printf '<code>%s</code>: %s<br>\n' "$key" "$desc"
done
printf '</p>\n'
}
{
echo "<h4>Default Mode Keybindings</h4>"
emit_keybinding_group "Applications" "${applications[@]}"
# Workspace Navigation
if [[ ${#workspaceSwitch[@]} -gt 0 ]] || [[ ${#workspaceMove[@]} -gt 0 ]]; then
echo "<p><strong>Workspace Navigation:</strong><br>"
echo "<code>Control+F1</code> through <code>F10</code>: Switch to workspace 1-10<br>"
echo "<code>Control+Shift+F1</code> through <code>F10</code>: Move window to workspace 1-10<br>"
echo "</p>"
fi
emit_keybinding_group "Window Management" "${windowMgmt[@]}"
emit_keybinding_group "Utilities" "${utilities[@]}"
emit_keybinding_group "Modes" "${modes[@]}"
emit_keybinding_group "Media Controls" "${media[@]}"
} > "${i3Path}/toplevel_temp.txt"
# Replace TOPLEVELKEYBINDINGS placeholder with the generated HTML content
awk '
/TOPLEVELKEYBINDINGS/ {
system("cat '"${i3Path}"'/toplevel_temp.txt")
next
}
{ print }
' "${i3Path}/I38.html" > "${i3Path}/I38.html.tmp" && mv "${i3Path}/I38.html.tmp" "${i3Path}/I38.html"
rm -f "${i3Path}/toplevel_temp.txt"
# Customize the html file to the user's choices.
I38_HELP_WEBBROWSER="$webBrowserHelp" \
I38_HELP_MODKEY="$mod" \
I38_HELP_SCREENREADER="$screenReaderHelp" \
I38_HELP_RATPOISONKEY="$escapeKey" \
I38_HELP_PERSONALMODEKEY="$personalModeKeyHelp" \
I38_HELP_TEXTEDITOR="$textEditorHelp" \
I38_HELP_FILEBROWSERHELP="$fileBrowserHelp" \
I38_HELP_CHECKOUTPATH="$i38CheckoutPath" \
python3 - "${i3Path}/I38.html" << 'PY'
import os
import sys
from pathlib import Path
html_path = Path(sys.argv[1])
replacements = {
"@WEBBROWSER@": os.environ["I38_HELP_WEBBROWSER"],
"@MODKEY@": os.environ["I38_HELP_MODKEY"],
"@SCREENREADER@": os.environ["I38_HELP_SCREENREADER"],
"@RATPOISONKEY@": os.environ["I38_HELP_RATPOISONKEY"],
"@PERSONALMODEKEY@": os.environ["I38_HELP_PERSONALMODEKEY"],
"@TEXTEDITOR@": os.environ["I38_HELP_TEXTEDITOR"],
"@FILEBROWSERHELP@": os.environ["I38_HELP_FILEBROWSERHELP"],
"@I38CHECKOUTPATH@": os.environ["I38_HELP_CHECKOUTPATH"],
}
content = html_path.read_text(encoding="utf-8")
for placeholder, value in replacements.items():
content = content.replace(placeholder, value)
html_path.write_text(content, encoding="utf-8")
PY
# Create the firstrun file
touch "${i3Path}/firstrun"
if [[ $defaultConfig -eq 1 ]]; then
echo "Default I38 configuration generated."
fi