Files
I38/i38.sh

1414 lines
50 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/>.
# Flag for sway configurations
usingSway=1 # Not by default.
i3Path="${XDG_CONFIG_HOME:-$HOME/.config}/i3"
i3msg="i3-msg"
configFile="${PWD}/I38_preferences.conf"
# Dialog accessibility
export DIALOGOPTS='--no-lines --visit-items'
# Check to make sure minimum requirements are installed.
for i in dialog jq yad xdotool ; do
if ! command -v "$i" &> /dev/null ; then
missing+=("$i")
fi
done
if ! python3 -c 'import i3ipc' &> /dev/null ; then
missing+=("python-i3ipc")
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.
dialog --clear --title "I38" --yesno "$*" -1 -1 --stdout
return $?
}
# 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"
}
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() {
# 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 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"
# Application paths
screenReader="$screenReader"
emailClient="$emailClient"
webBrowser="$webBrowser"
textEditor="$textEditor"
fileBrowser="$fileBrowser"
ircClient="$ircClient"
# Boolean settings (0=yes, 1=no)
udiskie="$udiskie"
dex="$dex"
batteryAlert="${batteryAlert:-1}"
brlapi="$brlapi"
sounds="$sounds"
# Screen lock
screenlockPinHash="$screenlockPinHash"
# Personal mode
personalModeEnabled="${personalModeEnabled:-1}"
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 ~/.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
fi
[ -f /etc/xprofile ] && . /etc/xprofile
[ -f ~/.xprofile ] && . ~/.xprofile
EOF
echo "$sessionCommand" >> ~/.xinitrc
chmod +x ~/.xinitrc
}
write_xprofile() {
if [[ -f "$HOME/.xprofile" ]]; then
if ! yesno "Would you like to add accessibility variables to your $HOME/.xprofile? Without these, accessibility will be limited or may not work at all. Do you want to continue?"; then
exit 0
fi
fi
cat << 'EOF' > ~/.xprofile
# Accessibility variables
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
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
}
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
}
# Array of command line arguments
declare -A command=(
[h]="This help screen."
[s]="Create sway configuration instead of i3."
[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;;
s)
swaySystemIncludesPath="/etc/sway/config.d"
usingSway=0
i3msg="swaymsg"
i3Path="${XDG_CONFIG_HOME:-$HOME/.config}/sway"
;;
u) update_scripts;;
x) write_xinitrc ;&
X) write_xprofile ;;
esac
done
# Load saved configuration if available
configLoaded=0
if 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 [[ ${#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
# Screen Reader
if [[ -z "$screenReader" ]] || ! command -v "$screenReader" &> /dev/null; then
programList=()
for i in cthulhu orca ; do
if command -v "${i/#-/}" &> /dev/null ; then
programList+=("$i")
fi
done
if [[ ${#programList[@]} -gt 1 ]]; then
screenReader="$(menulist ":Screen Reader" "${programList[@]}")"
else
screenReader="${programList[0]#-}"
fi
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
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 [[ -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 [[ -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 [[ -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 [[ -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
# 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 ]]; then
dex -t "${XDG_CONFIG_HOME:-${HOME}/.config}/autostart" -c "$(command -v "$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 [[ -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 [[ -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)
if [[ $configLoaded -eq 0 ]]; then
save_config
fi
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}"
# 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"
# Generated by I38 (${0##*/}) https://git.stormux.org/storm/I38
# $(date '+%A, %B %d, %Y at %I:%M%p')
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"
# Include your distribution Sway configuration files.
include ${swaySystemIncludesPath}/*
EOF
fi
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. Will also be used by the bar unless a different font
# is used in the bar {} block below.
font pango:monospace 8
# 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.sh
# Bookmarks dialog
bindsym \$mod+Control+b exec ${i3Path}/scripts/bookmarks.sh
# Clipboard manager
bindsym \$mod+Control+c exec clipster -s
# 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}/scrip/ts/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 ${i3Path}/scripts/i3-sensible-terminal.sh
# kill 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
# enter fullscreen mode for the focused container
bindsym \$mod+BackSpace fullscreen toggle
# Move the currently focused window to the scratchpad
bindsym \$mod+Shift+minus move scratchpad
# Bind the currently focused window to the scratchpad
# This means it will always open in the scratchpad
bindsym \$mod+Shift+equal exec --no-startup-id ${i3Path}/scripts/bind_to_scratchpad.sh
# Show the next scratchpad window or hide the focused scratchpad window.
# If there are multiple scratchpad windows, this command cycles through them.
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 [[ -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"
# 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"
# Panel mode configuration
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)
# 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 [[ -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
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 ${i3Path}/scripts/i3-sensible-terminal.sh, 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"
# Get a list of windows in the current workspace
bindsym apostrophe exec --no-startup-id ${i3Path}/scripts/window_list.sh, mode "default"
# Restart Cthulhu
bindsym Shift+c exec $(command -v cthulhu) --replace, mode "default"
# Restart Orca
bindsym Shift+o exec $(command -v orca) --replace, mode "default"
# Toggle screen reader
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\""
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 "# 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\""
fi)
# Run dialog with exclamation
bindsym Shift+exclam exec ${i3Path}/scripts/run_dialog.sh, 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
if [[ $usingSway -eq 0 ]]; then
echo "exec --no-startup-id ${i3Path}/scripts/sound.py"
else
echo "exec_always --no-startup-id ${i3Path}/scripts/sound.py"
fi
fi
# Steam game focus handler (i3 only) - focuses games when they open behind Big Picture
if [[ $usingSway -ne 0 ]] && 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
if [[ $usingSway -ne 0 ]]; then
echo "exec_always --no-startup-id ${i3Path}/scripts/i3_watchdog.sh"
fi
# xbrlapi is X11-only, skip on Sway/Wayland
if [[ $brlapi -eq 0 ]] && [[ $usingSway -ne 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 [[ $usingSway -eq 0 ]]; then
# Sway: prefer Wayland-native notification daemons
if command -v mako &> /dev/null; then
echo 'exec_always --no-startup-id mako'
elif [[ -x "/usr/lib/xfce4/notifyd/xfce4-notifyd" ]]; then
# Fallback to X11 variant via XWayland
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
echo 'exec_always --no-startup-id /usr/libexec/notification-daemon -r'
fi
else
# i3: use X11 notification daemons
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
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
# 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
if [[ $dex -eq 0 ]]; then
echo '# Start XDG autostart .desktop files using dex. See also'
echo '# https://wiki.archlinux.org/index.php/XDG_Autostart'
if [[ $usingSway -eq 0 ]]; then
echo 'exec --no-startup-id dex --autostart --environment sway'
else
echo 'exec --no-startup-id dex --autostart --environment i3'
fi
else
echo '# Startup applications'
# x11bell is X11-only, skip on Sway/Wayland
if command -v x11bell &> /dev/null && [[ $usingSway -ne 0 ]]; then
echo 'exec --no-startup-id x11bell play -nqV0 synth .1 sq norm -12'
fi
# Clipboard manager
if [[ $usingSway -eq 0 ]]; then
# Sway: use Wayland clipboard manager
if command -v wl-paste &> /dev/null && command -v clipman &> /dev/null; then
echo 'exec wl-paste -t text --watch clipman store'
fi
else
# i3: use X11 clipboard manager
echo 'exec --no-startup-id clipster -d'
fi
echo "exec $screenReader"
echo "exec_always --no-startup-id ${i3Path}/scripts/desktop.sh"
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 -t html "I38.md" -so "${i3Path}/I38.html" --metadata title="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##*/}"
# 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 (first 1-2 words before 'exec', 'workspace', etc)
keyCombo=$(echo "$keybinding" | awk '{print $1, $2}' | sed 's/workspace.*//' | sed 's/exec.*//' | sed 's/kill.*//' | sed 's/fullscreen.*//' | sed 's/scratchpad.*//' | sed 's/mode.*//' | sed 's/ *$//')
# Clean up key combo
keyCombo="${keyCombo//\$mod/$mod}"
keyCombo="${keyCombo//Mod1/Alt}"
keyCombo="${keyCombo//Mod4/Super}"
# Categorize based on the comment or the action
if [[ "$keybinding" =~ workspace.*number ]] && [[ "$keybinding" =~ Control+F ]]; then
# Switch to workspace
workspaceSwitch+=("$keyCombo|$lastComment")
elif [[ "$keybinding" =~ move.*container.*workspace ]] && [[ "$keybinding" =~ Control+Shift+F ]]; then
# Move to workspace
workspaceMove+=("$keyCombo|$lastComment")
elif [[ "$lastComment" =~ (menu|terminal|editor|browser|Run dialog) ]] || [[ "$keybinding" =~ (menu.py|sensible-terminal|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 < <(sed -n '/^mode "/,/^}$/!p' "${i3Path}/config" | grep -E '^#|^bindsym')
# Build HTML tables
{
echo "<h4>Default Mode Keybindings</h4>"
# Applications
if [[ ${#applications[@]} -gt 0 ]]; then
echo "<p><strong>Applications:</strong></p>"
echo "<table><thead><tr><th>Key</th><th>Action</th></tr></thead><tbody>"
for item in "${applications[@]}"; do
IFS='|' read -r key desc <<< "$item"
echo "<tr><td><code>$key</code></td><td>$desc</td></tr>"
done
echo "</tbody></table>"
fi
# Workspace Navigation
if [[ ${#workspaceSwitch[@]} -gt 0 ]] || [[ ${#workspaceMove[@]} -gt 0 ]]; then
echo "<p><strong>Workspace Navigation:</strong></p>"
echo "<table><thead><tr><th>Key</th><th>Action</th></tr></thead><tbody>"
echo "<tr><td><code>Control+F1</code> through <code>F10</code></td><td>Switch to workspace 1-10</td></tr>"
echo "<tr><td><code>Control+Shift+F1</code> through <code>F10</code></td><td>Move window to workspace 1-10</td></tr>"
echo "</tbody></table>"
fi
# Window Management
if [[ ${#windowMgmt[@]} -gt 0 ]]; then
echo "<p><strong>Window Management:</strong></p>"
echo "<table><thead><tr><th>Key</th><th>Action</th></tr></thead><tbody>"
for item in "${windowMgmt[@]}"; do
IFS='|' read -r key desc <<< "$item"
echo "<tr><td><code>$key</code></td><td>$desc</td></tr>"
done
echo "</tbody></table>"
fi
# Utilities
if [[ ${#utilities[@]} -gt 0 ]]; then
echo "<p><strong>Utilities:</strong></p>"
echo "<table><thead><tr><th>Key</th><th>Action</th></tr></thead><tbody>"
for item in "${utilities[@]}"; do
IFS='|' read -r key desc <<< "$item"
echo "<tr><td><code>$key</code></td><td>$desc</td></tr>"
done
echo "</tbody></table>"
fi
# Modes
if [[ ${#modes[@]} -gt 0 ]]; then
echo "<p><strong>Modes:</strong></p>"
echo "<table><thead><tr><th>Key</th><th>Action</th></tr></thead><tbody>"
for item in "${modes[@]}"; do
IFS='|' read -r key desc <<< "$item"
echo "<tr><td><code>$key</code></td><td>$desc</td></tr>"
done
echo "</tbody></table>"
fi
# Media (if any)
if [[ ${#media[@]} -gt 0 ]]; then
echo "<p><strong>Media Controls:</strong></p>"
echo "<table><thead><tr><th>Key</th><th>Action</th></tr></thead><tbody>"
for item in "${media[@]}"; do
IFS='|' read -r key desc <<< "$item"
echo "<tr><td><code>$key</code></td><td>$desc</td></tr>"
done
echo "</tbody></table>"
fi
} > "${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.
sed -i -e "s|BROWSER|${webBrowser}|g" \
-e "s|MODKEY|${mod}|g" \
-e "s|SCREENREADER|${screenReader}|g" \
-e "s|RATPOISONKEY|${escapeKey}|g" \
-e "s|TEXTEDITOR|${textEditor}|g" \
-e "s|FILEBROWSER|${fileBrowser}|g" "${i3Path}/I38.html"
# Create the firstrun file
touch "${i3Path}/firstrun"