Files
I38/i38.sh

1404 lines
49 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 ; 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 $?
}
# Custom application keybinding functions
declare -A usedKeys
declare -a customApps
showKeybindingHelp() {
dialog --title "I38 Keybinding Help" --msgbox \
"Keybinding Notation:
Modifiers: ^ = Ctrl, ! = Alt, # = Super, m = your mod key
Special keys: f1-f12, up/down/left/right, space, tab, return
home/end/insert/delete/pageup/pagedown, print
backspace, escape
Examples:
c = just 'c' key
^c = Ctrl+c
!f1 = Alt+F1
mspace = mod+Space
^!up = Ctrl+Alt+Up
#pagedown = Super+Page_Down
Uppercase letters imply Shift (e.g., C = Shift+c)" 0 0
}
convertKeybinding() {
local input="$1"
local result=""
local baseKey=""
# Handle modifiers
if [[ "$input" == *"^"* ]]; then
result+="Control+"
input="${input//^/}"
fi
if [[ "$input" == *"!"* ]]; then
result+="Mod1+"
input="${input//!/}"
fi
if [[ "$input" == *"#"* ]]; then
result+="Mod4+"
input="${input//#/}"
fi
if [[ "$input" == *"m"* ]]; then
result+="\$mod+"
input="${input//m/}"
fi
# Handle shift for uppercase letters
if [[ "$input" =~ [A-Z] ]]; then
result+="Shift+"
input="${input,,}"
fi
# Convert special keys
case "$input" in
f[1-9]|f1[0-2]) baseKey="F${input#f}" ;;
up|down|left|right) baseKey="$input" ;;
space) baseKey="space" ;;
tab) baseKey="Tab" ;;
return) baseKey="Return" ;;
escape) baseKey="Escape" ;;
backspace) baseKey="BackSpace" ;;
print) baseKey="Print" ;;
home) baseKey="Home" ;;
end) baseKey="End" ;;
insert) baseKey="Insert" ;;
delete) baseKey="Delete" ;;
pageup) baseKey="Page_Up" ;;
pagedown) baseKey="Page_Down" ;;
*) baseKey="$input" ;;
esac
echo "${result}${baseKey}"
}
populateUsedKeys() {
# Populate with existing ratpoison mode bindings
usedKeys["Shift+slash"]=1
usedKeys["c"]=1
usedKeys["e"]=1
usedKeys["f"]=1
usedKeys["\$mod+e"]=1
usedKeys["w"]=1
usedKeys["k"]=1
usedKeys["m"]=1
usedKeys["Print"]=1
usedKeys["\$mod+r"]=1
usedKeys["p"]=1
usedKeys["\$mod+s"]=1
usedKeys["Mod1+Shift+0"]=1
usedKeys["Mod1+Shift+9"]=1
usedKeys["Mod1+Shift+equal"]=1
usedKeys["Mod1+Shift+minus"]=1
usedKeys["Mod1+Shift+z"]=1
usedKeys["Mod1+Shift+c"]=1
usedKeys["Mod1+Shift+x"]=1
usedKeys["Mod1+Shift+v"]=1
usedKeys["Mod1+Shift+b"]=1
usedKeys["Mod1+Shift+u"]=1
usedKeys["Mod1+b"]=1
usedKeys["g"]=1
usedKeys["apostrophe"]=1
usedKeys["Shift+c"]=1
usedKeys["Shift+o"]=1
usedKeys["Shift+t"]=1
usedKeys["Control+semicolon"]=1
usedKeys["Control+Shift+semicolon"]=1
usedKeys["Shift+exclam"]=1
usedKeys["\$mod+q"]=1
usedKeys["Control+\$mod+q"]=1
usedKeys["Escape"]=1
usedKeys["Control+g"]=1
}
inputText() {
# Args: prompt text
dialog --title "I38" --inputbox "$1" 0 0 --stdout
}
addCustomApplication() {
local appName appCommand appFlags keybinding convertedKey
populateUsedKeys
while true; do
appName="$(inputText "Custom Applications:\n\nEnter application name (or press enter when finished):")"
[[ -z "$appName" ]] && break
appCommand="$(inputText "Enter execution path/command for $appName:")"
[[ -z "$appCommand" ]] && continue
appFlags="$(inputText "Enter command line flags for $appName (optional):")"
while true; do
keybinding="$(inputText "Enter keybinding for $appName (Examples: c, ^c, !f1, mspace, ^!up) or ? for help:")"
if [[ "$keybinding" == "?" ]]; then
showKeybindingHelp
continue
fi
[[ -z "$keybinding" ]] && break
convertedKey="$(convertKeybinding "$keybinding")"
if [[ -n "${usedKeys[$convertedKey]}" ]]; then
dialog --title "I38" --msgbox "Keybinding '$keybinding' ($convertedKey) is already in use. Please choose another." 0 0
continue
fi
# Add to arrays
customApps+=("$appName|$appCommand|$appFlags|$convertedKey")
usedKeys["$convertedKey"]=1
break
done
done
}
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
# Reconstruct customApps array from numbered entries
customApps=()
local i=0
local varName
while : ; do
varName="customApp_$i"
if [[ -n "${!varName}" ]]; then
customApps+=("${!varName}")
((i++))
else
break
fi
done
return 0
fi
return 1
}
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"
# Custom applications
EOF
# Save custom apps with numbered keys
for i in "${!customApps[@]}"; do
echo "customApp_$i=\"${customApps[$i]}\"" >> "$configFile"
done
dialog --title "I38" --msgbox "Configuration saved to $configFile\n\nYou can edit this file manually or delete it to reconfigure from scratch." 0 0
fi
}
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
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
[ -x "$f" ] && . "$f"
done
unset f
fi
[ -f /etc/xprofile ] && . /etc/xprofile
[ -f ~/.xprofile ] && . ~/.xprofile
exec dbus-run-session -- i3
EOF
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
}
update_scripts() {
cp -rv scripts/ "${i3Path}/" | dialog --backtitle "I38" --progressbox "Updating scripts..." -1 -1
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
if [[ -f "${waytrayConfig}" ]]; 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
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
unset programList
for i in cthulhu orca ; do
if command -v ${i/#-/} &> /dev/null ; then
if [ -n "$programList" ]; then
programList="$programList $i"
else
programList="$i"
fi
fi
done
if [ "$programList" != "${programList// /}" ]; then
screenReader="$(menulist ":Screen Reader" $programList)"
else
screenReader="${programList/#-/}"
fi
export screenReader="$(command -v $screenReader)"
else
# Validate and export existing preference
export screenReader
fi
# Email client
if [[ -z "$emailClient" ]] || ! command -v "$emailClient" &> /dev/null; then
unset programList
for i in betterbird evolution thunderbird ; do
if command -v ${i/#-/} &> /dev/null ; then
if [ -n "$programList" ]; then
programList="$programList $i"
else
programList="$i"
fi
fi
done
if [ "$programList" != "${programList// /}" ]; then
emailClient="$(menulist "Email client:" $programList)"
else
emailClient="${programList/#-/}"
fi
export emailClient="$(command -v $emailClient)"
else
# Validate and export existing preference
export emailClient
fi
# Web browser
if [[ -z "$webBrowser" ]] || ! command -v "$webBrowser" &> /dev/null; then
unset programList
for i in brave chromium epiphany firefox google-chrome-stable google-chrome-unstable microsoft-edge-stable microsoft-edge-beta microsoft-edge-dev midori seamonkey vivaldi ; do
if command -v ${i/#-/} &> /dev/null ; then
if [ -n "$programList" ]; then
programList="$programList $i"
else
programList="$i"
fi
fi
done
if [ "$programList" != "${programList// /}" ]; then
webBrowser="$(menulist "Web browser:" $programList)"
else
webBrowser="${programList/#-/}"
fi
export webBrowser="$(command -v $webBrowser)"
else
# Validate and export existing preference
export webBrowser
fi
# Text editor
if [[ -z "$textEditor" ]] || ! command -v "$textEditor" &> /dev/null; then
unset programList
for i in emacs geany gedit kate kwrite l3afpad leafpad libreoffice mousepad pluma ; do
if hash ${i/#-/} &> /dev/null ; then
if [ -n "$programList" ]; then
programList="$programList $i"
else
programList="$i"
fi
fi
done
if [ "$programList" != "${programList// /}" ]; then
textEditor="$(menulist "Text editor:" $programList)"
else
textEditor="${programList/#-/}"
fi
export textEditor="$(command -v $textEditor)"
else
# Validate and export existing preference
export textEditor
fi
# File browser
if [[ -z "$fileBrowser" ]] || ! command -v "$fileBrowser" &> /dev/null; then
unset programList
for i in caja nemo nautilus pcmanfm pcmanfm-qt thunar ; do
if hash ${i/#-/} &> /dev/null ; then
if [ -n "$programList" ]; then
programList="$programList $i"
else
programList="$i"
fi
fi
done
if [ "$programList" != "${programList// /}" ]; then
fileBrowser="$(menulist "File browser:" $programList)"
else
fileBrowser="${programList/#-/}"
fi
export fileBrowser="$(command -v $fileBrowser)"
else
# Validate and export existing preference
export fileBrowser
fi
# IRC client
if [[ -z "$ircClient" ]] || ! command -v "$ircClient" &> /dev/null; then
unset programList
for i in albikirc Albikirc access-irc ; do
if command -v ${i/#-/} &> /dev/null ; then
if [ -n "$programList" ]; then
programList="$programList $i"
else
programList="$i"
fi
fi
done
if [ "$programList" != "${programList// /}" ]; then
ircClient="$(menulist "IRC client:" $programList)"
else
ircClient="${programList/#-/}"
fi
export ircClient="$(command -v $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
# Custom applications for ratpoison mode
if [[ ${#customApps[@]} -eq 0 ]]; then
addCustomApplication
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
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
# 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"
$(if command -v lxsession-logout &> /dev/null ; then
echo "# Power options bound to p"
echo "bindsym p exec --no-startup-id lxsession-logout, mode \"default\""
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"
# 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)
# Custom applications
$(for app in "${customApps[@]}"; do
IFS='|' read -r appName appCommand appFlags appKey <<< "$app"
echo "# $appName bound to $appKey"
if [[ -n "$appFlags" ]]; then
echo "bindsym $appKey exec $appCommand $appFlags, mode \"default\""
else
echo "bindsym $appKey exec $appCommand, mode \"default\""
fi
done)
# Run dialog with exclamation
bindsym Shift+exclam exec ${i3Path}/scripts/run_dialog.sh, mode "default"
# exit i3 (logs you out of your X session)
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
# 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"