Files
I38/i38.sh

1171 lines
42 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"
# 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 [[ -n "${missing}" ]]; 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
echo $?
}
# 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
}
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
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
continue="$(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?")"
if [ "$continue" = "no" ]; 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
}
# 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
# 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
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
# Multiple keyboard layouts
if [[ $(yesno "Do you want to use multiple keyboard layouts?") -eq 0 ]]; then
unset kbd
while : ; do
kbd+=("$(keyboard_menu)") || break
done
fi
# Volume jump
volumeJump=$(rangebox "How much should pressing the volume keys change the volume?" 1 15 5)
# Screen Reader
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)"
# Email client
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)"
# Web browser
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)"
# Text editor
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)"
# File browser
# Configure file browser
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)"
# Auto mount removable media
udiskie=1
if command -v udiskie &> /dev/null ; then
export udiskie=$(yesno "Would you like removable drives to automatically mount when plugged in?")
fi
# Auto start with dex
dex=1
if command -v dex &> /dev/null ; then
export dex=$(yesno "Would you like to autostart applications with dex?")
fi
if [[ $dex -eq 0 ]]; then
dex -t "${XDG_CONFIG_HOME:-${HOME}/.config}/autostart" -c $(command -v $screenReader)
fi
if command -v acpi &> /dev/null ; then
batteryAlert=1
batteryAlert=$(yesno "Do you want low battery notifications?")
fi
brlapi=1
brlapi=$(yesno "Do you want to use a braille display with ${screenReader##*/}?")
sounds=1
sounds=$(yesno "Do you want window event sounds?")
# Custom applications for ratpoison mode
addCustomApplication
if [[ -d "${i3Path}" ]]; then
yesno "This will replace your existing configuration at ${i3Path}. Do you want to continue?" || exit 0
fi
# 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
# 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"
# 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"
$(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)
$(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 $sensibleTerminal, 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 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
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##*/}"
# 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"