UP  |  HOME

i3

Table of Contents

Main Config

Since I'm using i3 4.19 I don't have access to the new 'include' directive in 4.20. As a workaround for device specific settings I'm using yadm's templating feature so I've included that template below instead of a working i3 config.

Listing 1: ~/.config/i3/config##template
# DO NOT EDIT ~/.config/i3/config
# MODIFY YOUR TEMPLATE ~/.config/i3/config##template

# colors used, for reference
#333333 dark grey
#56ADBC teal blue
#666666 charcoal grey
#8C6BC8 deep lavendar
#AE81FF liliac
#E3E3DD light grey
#FA9429 pumpkin
#66D9EF robin's egg

# Window Colors
# class                 border  backgr. text    indicator child_border
client.focused          #333333 #56ADBC #333333 #FA9429   #56ADBC
client.unfocused        #333333 #333333 #E3E3DD #333333   #333333
client.focused_inactive #666666 #333333 #E3E3DD #333333   #333333
client.urgent           #AE81FF #8C6BC8 #E3E3DD #8C6BC8   #8C6BC8
client.placeholder      #333333 #333333 #E3E3DD #333333   #333333

client.background       #E3E3DD

# i3 config file (v4)
#
# Please see http://i3wm.org/docs/userguide.html for a complete reference!
show_marks no

set $mod Mod4

{% if yadm.hostname == "delta" %}
font pango:Fira Code 11
{% else %}
font pango:Fira Code 9
{% endif %}

# start a terminal
bindsym $mod+grave        exec urxvt

# rox-filer shortcuts
bindsym $mod+Tab exec rox-filer

# kill focused window
bindsym $mod+Shift+apostrophe kill

# start dmenu (a program launcher)
bindsym $mod+p exec dmenu_run

# There also is the (new) i3-dmenu-desktop which only displays applications
# shipping a .desktop file. It is a wrapper around dmenu, so you need that
# installed.
bindsym $mod+y exec --no-startup-id i3-dmenu-desktop

# change focus
bindsym $mod+h focus left
bindsym $mod+l focus right
bindsym $mod+k focus up
bindsym $mod+j focus down

# move focused window
bindsym $mod+Shift+h move left
bindsym $mod+Shift+l move right
bindsym $mod+Shift+k move up
bindsym $mod+Shift+j move down

# switch workspaces
bindsym $mod+Left  workspace prev
bindsym $mod+Right workspace next

# Rearrange workspaces
bindsym $mod+Shift+Left  move workspace to output left
bindsym $mod+Shift+Down  move workspace to output down
bindsym $mod+Shift+Up    move workspace to output up
bindsym $mod+Shift+Right move workspace to output right

# split in horizontal orientation
bindsym $mod+s split h

# split in vertical orientation
bindsym $mod+v split v

# enter fullscreen mode for the focused container
bindsym $mod+u       fullscreen toggle
bindsym $mod+Shift+u fullscreen toggle global

# change container layout (stacked, tabbed, toggle split)
bindsym $mod+apostrophe layout stacking
bindsym $mod+comma      layout tabbed
bindsym $mod+period     layout toggle split

# Use Mouse+$mod to drag floating windows to their wanted position
floating_modifier $mod

# toggle tiling / floating
bindsym $mod+Shift+space floating toggle

# change focus between tiling / floating windows
bindsym $mod+space focus mode_toggle

# always float geeqie
for_window [class="Geeqie"] floating enable
for_window [class="Gthumb"] floating enable
#for_window [class="Remmina"] floating enable

# scratchpad
bindsym $mod+shift+z move scratchpad
bindsym $mod+shift+v scratchpad show

# focus the parent container
bindsym $mod+a       focus parent
# focus the child container
bindsym $mod+Shift+a focus child

# switch to workspace
bindsym $mod+1   workspace number 1
bindsym $mod+2   workspace number 2
bindsym $mod+3   workspace number 3
bindsym $mod+4   workspace number 4
bindsym $mod+5   workspace number 5
bindsym $mod+6   workspace number 6
bindsym $mod+7   workspace number 7
bindsym $mod+8   workspace number 8
bindsym $mod+9   workspace number 9
bindsym $mod+0   workspace number 10
bindsym $mod+F1  workspace number 11
bindsym $mod+F2  workspace number 12
bindsym $mod+F3  workspace number 13
bindsym $mod+F4  workspace number 14
bindsym $mod+F5  workspace number 15
bindsym $mod+F6  workspace number 16
bindsym $mod+F7  workspace number 17
bindsym $mod+F8  workspace number 18
bindsym $mod+F9  workspace number 19
bindsym $mod+F10 workspace number 20

# move focused container to workspace
bindsym $mod+Shift+1   move container to workspace number 1
bindsym $mod+Shift+2   move container to workspace number 2
bindsym $mod+Shift+3   move container to workspace number 3
bindsym $mod+Shift+4   move container to workspace number 4
bindsym $mod+Shift+5   move container to workspace number 5
bindsym $mod+Shift+6   move container to workspace number 6
bindsym $mod+Shift+7   move container to workspace number 7
bindsym $mod+Shift+8   move container to workspace number 8
bindsym $mod+Shift+9   move container to workspace number 9
bindsym $mod+Shift+0   move container to workspace number 10
bindsym $mod+Shift+F1  move container to workspace number 11
bindsym $mod+Shift+F2  move container to workspace number 12
bindsym $mod+Shift+F3  move container to workspace number 13
bindsym $mod+Shift+F4  move container to workspace number 14
bindsym $mod+Shift+F5  move container to workspace number 15
bindsym $mod+Shift+F6  move container to workspace number 16
bindsym $mod+Shift+F7  move container to workspace number 17
bindsym $mod+Shift+F8  move container to workspace number 18
bindsym $mod+Shift+F9  move container to workspace number 19
bindsym $mod+Shift+F10 move container to workspace number 20

# new workspace
bindsym $mod+n exec "i3-msg workspace number $(($(i3-msg -t get_workspaces |jq -r '.[].name'|cut -d':' -f 1|sort -n| tail -1) + 1))"

# work with workspaces
bindsym $mod+Shift+r      exec i3-msg rename workspace \
    $(i3-msg -t get_workspaces | jq '.[] | select(.focused==true).name') \
    to \
    $(i3-msg -t get_workspaces | jq '.[] | select(.focused==true).num'):$(zenity --entry --title 'Rename workspace' --text "New workspace name:")
bindsym $mod+Mod1+Shift+m exec "~/.config/i3/ws_main.sh"
bindsym $mod+Mod1+Shift+c exec "~/.config/i3/ws_new_code.sh"
bindsym $mod+Mod1+Shift+d exec "~/.config/i3/ws_new_doc.sh"

# remark windows that are already swallowed
for_window [class="^Atril$|^Devhelp$|^Evince$|^Gvim$|^calibre$" workspace="^\d+:doc" con_mark="^swallowed\d$"] mark --replace unswallowed
# toggle it. (swallowed windows become unmarked, unswallowed windows are now marked)
for_window [class="^Atril$|^Devhelp$|^Evince$|^Gvim$|^calibre$" workspace="^\d+:doc"] mark --toggle unswallowed
# move unswallowed windows to the desired mark
for_window [class="^Atril$|^Devhelp$|^Evince$|^Gvim$|^calibre$" workspace="^\d+:doc" con_mark="^unswallowed$"] move window to mark doc

# Lock with xscreensaver (Hyper+Alt+l)
{% if yadm.hostname == "dreadnought" %}
# Save screenshots of all workspaces, kill steam because it breaks DPMS, then lock
bindsym $mod+Mod1+l exec "~/.scripts/i3_screenshot.py; killall steam; xscreensaver-command -lock"
{% else %}
bindsym $mod+Mod1+l exec "xscreensaver-command -lock"
{% endif %}

# reload the configuration file
bindsym $mod+Shift+y reload
# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)
bindsym $mod+Shift+p restart
# exit i3 (logs you out of your X session)
bindsym $mod+Shift+period exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -b 'Yes, exit i3' 'i3-msg exit'"

# mouse actions
bindsym --release $mod+button2 kill
# Issue #2523 numlock is somehow Mod2. dumb.
bindsym --release Mod2+button10 kill 
bindsym --release button10 kill 
bindsym --release $mod+button3 floating toggle

# move all but main (1) workspace to chosen output
# (uses xrandr, grep, cut, zenity, i3-msg, jq)
bindsym $Mod+Shift+m exec "~/.config/i3/mv_ws.sh"

# change screen layout
mode "screenlayout" {
    bindsym grave exec autorandr common; mode "default"
    bindsym 1 exec autorandr --load "Magedok 9 - |⠀⡀⠀⠀|"; mode "default"
    bindsym 2 exec autorandr --load "U2414H - |⠰⠶⠀⠀|"; mode "default"
    bindsym 3 exec autorandr --load "U2515H - |⠀⠀⠿⠿|"; mode "default"
    bindsym 4 exec autorandr --load "MageDok 15 - |⠈⠁⠀⠀|"; mode "default"

    bindsym 5 exec autorandr --load "All - |⠸⡷⠿⠿|"; mode "default"

    # new background
    bindsym 0  exec --no-startup-id ~/.fehbg
    # reset background if it gets shifted
    bindsym 9  exec --no-startup-id feh --no-fehbg --bg-fill "$(cat ~/.last_fehbg)"

    bindsym t  exec ~/.screenlayout/touch_display.sh

    bindsym $mod+x mode "default"
    bindsym Return mode "default"
    bindsym Escape mode "default"
}

bindsym $mod+x mode "screenlayout"
bindsym $mod+Shift+x exec "~/.screenlayout/autorandr.sh"

#Toggle touchscreen/touchpad
bindsym $mod+Shift+s exec --no-startup-id notify-send "$(~/.config/i3/toggle_ts.sh)" -t 1400
bindsym $mod+Shift+t exec --no-startup-id notify-send "$(~/.config/i3/toggle_tp.sh)" -t 1400

# Multimedia, brightness keys
{% if yadm.hostname == "dreadnought" %}
bindsym XF86AudioRaiseVolume  exec --no-startup-id pactl set-sink-volume alsa_output.usb-0d8c_USB_Sound_Device-00.analog-stereo +3% && pactl set-sink-volume alsa_output.pci-0000_0f_00.6.analog-stereo +3%
bindsym XF86AudioLowerVolume  exec --no-startup-id pactl set-sink-volume alsa_output.usb-0d8c_USB_Sound_Device-00.analog-stereo -3% && pactl set-sink-volume alsa_output.pci-0000_0f_00.6.analog-stereo -3%
bindsym XF86AudioMute         exec --no-startup-id pactl set-sink-mute alsa_output.usb-0d8c_USB_Sound_Device-00.analog-stereo toggle && pactl set-sink-mute alsa_output.pci-0000_0f_00.6.analog-stereo toggle
{% else %}
bindsym XF86AudioRaiseVolume  exec --no-startup-id pactl set-sink-volume 0 +3%
bindsym XF86AudioLowerVolume  exec --no-startup-id pactl set-sink-volume 0 -3%
bindsym XF86AudioMute         exec --no-startup-id pactl set-sink-mute 0 toggle
{% endif %}

bindsym XF86Display           exec --no-startup-id lxrandr
bindsym XF86MonBrightnessUp   exec brightnessctl s +10%
bindsym XF86MonBrightnessDown exec brightnessctl s 10%-

bindsym XF86AudioPlay         exec playerctl play-pause
bindsym XF86AudioPrev         exec playerctl previous
bindsym XF86AudioNext         exec playerctl next
bindsym $mod+Shift+Mod1+space exec playerctl play-pause
bindsym $mod+Up               exec playerctl previous
bindsym $mod+Down             exec playerctl next

# Start i3bar to display a workspace bar (plus the system information i3status
# finds out, if available)
bar {
    status_command i3status
    tray_output    primary
    bindsym button2 exec "i3-msg workspace number $(($(i3-msg -t get_workspaces |jq -r '.[].name'|cut -d':' -f 1|sort -n| tail -1) + 1))"
    bindsym button4 workspace prev_on_output
    bindsym button5 workspace next_on_output
    colors {
        background #1B1D1E
        statusline #E3E3DD
        focused_workspace #666666 #66D9EF #333333
        active_workspace  #666666 #333333 #56ADBC
        urgent_workspace  #666666 #FA9429 #333333
        binding_mode      #666666 #FA9429 #333333
    }
}

# cursor setup, background
exec --no-startup-id xsetroot -cursor_name left_ptr
exec --no-startup-id ~/.fehbg

# autostart apps
exec --no-startup-id picom
exec --no-startup-id /usr/lib/x86_64-linux-gnu/xfce4/notifyd/xfce4-notifyd
exec --no-startup-id keepassx
exec --no-startup-id remmina -i
exec --no-startup-id krb5-auth-dialog
exec --no-startup-id pasystray
exec --no-startup-id nm-applet
exec --no-startup-id udiskie --tray
exec --no-startup-id xfce4-notes
exec --no-startup-id kdeconnect-indicator
exec --no-startup-id blueman-applet
exec --no-startup-id usbguard-applet-qt
exec --no-startup-id fusuma --daemon

{% if yadm.hostname == "dreadnought" %}
exec --no-startup-id ~/.dropbox/dropbox_start.py
{% endif %}

{% if yadm.hostname == "delta" %}
exec --no-startup-id ~/.dropbox/dropbox_start.py
exec --no-startup-id brightnessctl --device='tpacpi::kbd_backlight' set 1
{% else %}
exec --no-startup-id xscreensaver -nosplash
{% endif %}

{% if yadm.hostname == "delta" %}

# remarkable vnsee display
# exec --no-startup-id xrandr --newmode 1404x1872 0 1404 1404 1404 1404 1872 1872 1872 1872
# exec --no-startup-id xrandr --addmode VIRTUAL1 1404x1872
# exec --no-startup-id xrandr --output VIRTUAL1 --mode 1404x1872 --right-of eDP1
# bindsym $mod+F11 exec "pkill -f 'x11vnc -repeat -forever -nocursor -allow 10.11.99.1'"
# bindsym $mod+F12 exec "x11vnc -repeat -forever -nocursor -allow 10.11.99.1 -nopw -clip $(xrandr | perl -n -e'/VIRTUAL1 .*?(\d+x\d+\+\d+\+\d+)/ && print $1')"
{% endif %}

##############################################
############ Русский layout binds ############
##############################################

bindsym $mod+less   exec urxvt

# kill focused window
bindsym $mod+Shift+Cyrillic_SHORTI kill

# start dmenu, i3-dmenu-desktop
bindsym $mod+Cyrillic_ka exec dmenu_run
bindsym $mod+Cyrillic_ie exec --no-startup-id i3-dmenu-desktop

# change focus
bindsym $mod+Cyrillic_o focus left
bindsym $mod+Cyrillic_ze focus right
bindsym $mod+Cyrillic_em focus up
bindsym $mod+Cyrillic_es focus down

# move focused window
bindsym $mod+Shift+Cyrillic_O move left
bindsym $mod+Shift+Cyrillic_ZE move right
bindsym $mod+Shift+Cyrillic_EM move up
bindsym $mod+Shift+Cyrillic_ES move down

# split in horizontal orientation
bindsym $mod+Cyrillic_zhe split h

# split in vertical orientation
bindsym $mod+Cyrillic_yu split v

# enter fullscreen mode for the focused container
bindsym $mod+Cyrillic_a fullscreen toggle
bindsym $mod+Shift+Cyrillic_A fullscreen toggle global

# change container layout (stacked, tabbed, toggle split)
bindsym $mod+Cyrillic_shorti layout stacking
bindsym $mod+Cyrillic_tse    layout tabbed
bindsym $mod+Cyrillic_u      layout toggle split

# scratchpad binds omitted, unused, and I don't want to bind the equivalent keys

# focus the parent container
bindsym $mod+Cyrillic_ef       focus parent
# focus the child container
bindsym $mod+Shift+Cyrillic_EF focus child

# new workspace
bindsym $mod+Cyrillic_de exec "i3-msg workspace number $(($(i3-msg -t get_workspaces |jq -r '.[].name'|cut -d':' -f 1|sort -n| tail -1) + 1))"

# work with workspaces
bindsym $mod+Shift+Cyrillic_SHCHA         exec i3-msg rename workspace \
    $(i3-msg -t get_workspaces | jq '.[] | select(.focused==true).name') \
    to \
    $(i3-msg -t get_workspaces | jq '.[] | select(.focused==true).num'):$(zenity --entry --title 'Rename workspace' --text "New workspace name:")
bindsym $mod+Mod1+Shift+Cyrillic_SOFTSIGN exec "~/.config/i3/ws_main.sh"
bindsym $mod+Mod1+Shift+Cyrillic_SHA      exec "~/.config/i3/ws_new_code.sh"
bindsym $mod+Mod1+Shift+Cyrillic_ER       exec "~/.config/i3/ws_new_doc.sh"

# Lock with xscreensaver (Hyper+Alt+l)
bindsym $mod+Mod1+Cyrillic_ze exec "xscreensaver-command -lock"

# reload the configuration file
bindsym $mod+Shift+Cyrillic_IE reload
# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)
bindsym $mod+Shift+Cyrillic_KA restart
# exit i3 (logs you out of your X session)
bindsym $mod+Shift+Cyrillic_U  exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -b 'Yes, exit i3' 'i3-msg exit'"

# move all but main (1) workspace to chosen output
# (uses xrandr, grep, cut, zenity, i3-msg, jq)
bindsym $Mod+Shift+Cyrillic_SOFTSIGN exec "~/.config/i3/mv_ws.sh"

# change screen layout
mode "screenlayout" {
bindsym $mod+Cyrillic_i mode "default"
}

bindsym $mod+Cyrillic_i mode "screenlayout"

#Toggle touchscreen/touchpad
bindsym $mod+Shift+Cyrillic_ZHE exec --no-startup-id notify-send "$(~/.config/i3/toggle_ts.sh)" -t 1400
bindsym $mod+Shift+Cyrillic_EL  exec --no-startup-id notify-send "$(~/.config/i3/toggle_tp.sh)" -t 1400

Helper utilites

dmenu

dmenu has been simple and fast and I haven't found much need to replace it. I did try rofi and liked it as well, but wasn't motivated enough to change.

i3bar

Stock i3 status and workspace bar

i3status

My default config:

Listing 2: ~/.config/i3status/config##default
# i3status configuration file.
# see "man i3status" for documentation.

# It is important that this file is edited as UTF-8.
# The following line should contain a sharp s:
# ß
# If the above line is not correctly displayed, fix your editor first!

general {
        colors = true
        color_good = "#cfd0c2"
        color_bad = "#fc981f"
        color_degraded = "#e7db75"
        interval = 5
}

order += "disk /"
order += "disk /home"
order += "wireless _first_"
order += "ethernet _first_"
order += "battery all"
order += "load"
order += "volume desktop"
order += "tztime local"

wireless _first_ {
        format_up = "W: (%quality at %essid) %ip"
        format_down = ""
}

ethernet _first_ {
        # if you use %speed, i3status requires root privileges
        format_up = "E: %ip (%speed)"
        format_down = ""
        color_degraded = "#cfd0c2"
}

battery all {
        format = "%status %percentage %remaining"
        format_down = ""
    status_chr = "⚡"
    status_unk = "⚡"
    status_bat = "🔋"
        low_threshold = 10
        threshold_type = time
        integer_battery_capacity = true
}

tztime local {
        # abbr. weekday YYYY-MM-DD HH:mm:ss
        format = "%a %F %T"
}

load {
        format = "%1min"
}

disk "/" {
        format = "root: %avail"
}

disk "/home" {
        format = "home: %avail"
}

volume desktop {
    format = "🔊: %volume"
        format_muted = "🔊: muted (%volume)"
        device = "pulse:0"
}

My main desktop config, notable for the volume blocks being manually specified:

Listing 3: ~/.config/i3status/config##o.Linux,h.dreadnought
# i3status configuration file.
# see "man i3status" for documentation.

# It is important that this file is edited as UTF-8.
# The following line should contain a sharp s:
# ß
# If the above line is not correctly displayed, fix your editor first!

general {
        colors = true
        color_good = "#cfd0c2"
        color_bad = "#fc981f"
        color_degraded = "#e7db75"
        interval = 5
}

order += "disk /"
order += "disk /home"
order += "wireless _first_"
order += "ethernet _first_"
order += "battery all"
order += "load"
order += "volume desktop"
order += "volume music"
order += "tztime local"

wireless _first_ {
    format_up = "W: (%quality at %essid) %ip"
    format_down = ""
}

ethernet _first_ {
    # if you use %speed, i3status requires root privileges
    format_up = "E: %ip (%speed)"
    format_down = ""
    color_degraded = "#cfd0c2"
}

battery all {
        format = "%status %percentage Rem:%remaining"
        format_down = ""
    status_chr = "⚡"
    status_bat = "🔋"
        low_threshold = 10
        threshold_type = time
        integer_battery_capacity = true
}

tztime local {
        # abbr. weekday YYYY-MM-DD HH:mm:ss
    format = "%a %F %T"
}

load {
    format = "%1min"
}

disk "/" {
    format = "root: %avail"
}

disk "/home" {
    format = "home: %avail"
}

volume desktop {
    format = "🔊: %volume"
    format_muted = "🔊: muted (%volume)"
    device = "pulse:alsa_output.pci-0000_0f_00.6.analog-stereo"
}

volume music {
    format = "♪: %volume"
    format_muted = "♪: muted (%volume)"
    device = "pulse:alsa_output.usb-0d8c_USB_Sound_Device-00.analog-stereo"
}

feh

For setting backgrounds. I have this small script to randomly select a file and track the previous wallpaper for reverting.

Listing 4: ~/.fehbg
#!/bin/sh
wallpaper_path="$(find ~/.wallpapers/|sort -R|tail -n 1)"
echo "$wallpaper_path" > ~/.last_fehbg
feh --no-fehbg --bg-fill "$wallpaper_path"

Scripts

Load a predefined workspace/layout

Main workspace

Listing 5: ~/.config/i3/ws_main.sh
#!/bin/bash

workspace_shape=$(i3-msg -t get_workspaces|jq -r '.[]|select (.focused==true)|if .rect.width >= .rect.height then "landscape" else "portrait" end')

# Select currently focused workspace number
workspace_number=$(($(i3-msg -t get_workspaces|jq -r '.[]|select (.focused==true)|.name' \
    |cut -d':' -f 1 |sort -n| tail -1)))

# Rename workspace
i3-msg rename workspace to "$workspace_number:main"

# Load appropriate layout
if [ "$workspace_shape" == "portrait" ]; then
    i3-msg append_layout "$HOME/.config/i3/main-portrait.json"
elif [ "$workspace_shape" == "landscape" ]; then
    i3-msg append_layout "$HOME/.config/i3/main-landscape.json"
fi

i3-msg "exec cantata; exec claws-mail; exec firefox; exec hexchat"

Main layouts

Listing 6: ~/.config/i3/main-portrait.json
// vim:ts=4:sw=4:et
{
    // tabbed split container with 3 children
    "border": "normal",
    "floating": "auto_off",
    "layout": "tabbed",
    "percent": 0.4,
    "type": "con",
    "nodes": [
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "percent": 0.333333333333333,
            "swallows": [
               {
                "class": "^Hexchat$",
                "instance": "^hexchat$"
               }
            ],
            "type": "con"
        },
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "percent": 0.333333333333333,
            "swallows": [
               {
                "class": "^Claws\\-mail$",
                "instance": "^claws\\-mail$"
               }
            ],
            "type": "con"
        },
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "percent": 0.333333333333333,
            "swallows": [
               {
                "class": "^cantata$",
                "instance": "^cantata$"
               }
            ],
            "type": "con"
        }
    ]
}

{
    // tabbed split container with 2 children
    "border": "normal",
    "floating": "auto_off",
    "layout": "tabbed",
    "percent": 0.6,
    "type": "con",
    "nodes": [
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "percent": 0.5,
            "swallows": [
               {
                   "class": "^Firefox\\-esr$",
                   "instance": "^Navigator$"
               }
            ],
            "type": "con"
        },
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "geometry": {
               "height": 1400,
               "width": 1772,
               "x": 1920,
               "y": 352
            },
            "name": "KeePassX",
            "percent": 0.5,
            "swallows": [
               {
                "class": "^Keepassx$",
                "instance": "^keepassx$"
               }
            ],
            "type": "con"
        }
    ]
}
Listing 7: ~/.config/i3/main-landscape.json
// vim:ts=4:sw=4:et
{
    // splitv split container with 2 children
    "border": "normal",
    "floating": "auto_off",
    "layout": "splitv",
    "percent": 0.365,
    "type": "con",
    "nodes": [
        {
            // tabbed split container with 2 children
            "border": "normal",
            "floating": "auto_off",
            "layout": "tabbed",
            "percent": 0.5,
            "type": "con",
            "nodes": [
                {
                    "border": "normal",
                    "current_border_width": 2,
                    "floating": "auto_off",
                    "percent": 0.333333333333333,
                    "swallows": [
                       {
                           "class": "^Claws\\-mail$",
                          "instance": "^claws\\-mail$"
                       }
                    ],
                    "type": "con"
                },
                {
                    "border": "normal",
                    "current_border_width": 2,
                    "floating": "auto_off",
                    "percent": 0.333333333333333,
                    "swallows": [
                       {
                          "class": "^cantata$",
                          "instance": "^cantata$"
                       }
                    ],
                    "type": "con"
                }
            ]
        },
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "percent": 0.5,
            "swallows": [
               {
                  "class": "^Hexchat$",
                  "instance": "^hexchat$"
               }
            ],
            "type": "con"
        }
    ]
}

{
    // tabbed split container with 2 children
    "border": "normal",
    "floating": "auto_off",
    "layout": "tabbed",
    "percent": 0.635,
    "type": "con",
    "nodes": [
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "percent": 0.5,
            "swallows": [
               {
                  "class": "^Keepassx$",
                  "instance": "^keepassx$"
               }
            ],
            "type": "con"
        },
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "percent": 0.5,
            "swallows": [
               {
                  "class": "^Firefox\\-esr$",
                  "instance": "^Navigator$"
               }
            ],
            "type": "con"
        }
    ]
}

Code workspace

Listing 8: ~/.config/i3/ws_new_code.sh
#!/bin/bash

workspace_shape=$(i3-msg -t get_workspaces|jq -r '.[]|select (.focused==true)|if .rect.width >= .rect.height then "landscape" else "portrait" end')

if [ "$workspace_shape" == "portrait" ]; then
    workspace_size=$(i3-msg -t get_workspaces|jq -r '.[]|select (.focused==true)|.rect.height')
elif [ "$workspace_shape" == "landscape" ]; then
    workspace_size=$(i3-msg -t get_workspaces|jq -r '.[]|select (.focused==true)|.rect.width')
fi

# Select next highest unused number
workspace_number=$(($(i3-msg -t get_workspaces |jq -r '.[].name' \
    |cut -d':' -f 1 |sort -n| tail -1) + 1))

# Switch to and rename workspace
i3-msg workspace number $workspace_number
if [ -d "$1" ]; then
    i3-msg rename workspace to "$workspace_number:$(basename "$1")"
else
    i3-msg rename workspace to "$workspace_number:code"
fi

# Load appropriate layout
if [ "$workspace_shape" == "portrait" ]; then
    # TODO make a portrait layout when the need arises
    # We still spawn a workplace for now because it's easy to select
    # all windows and move them to the correct monitor
    if [ "$workspace_size" -lt 2000 ]; then
        i3-msg append_layout "$HOME/.config/i3/code_1080_landscape.json"
    else
        i3-msg append_layout "$HOME/.config/i3/code_2560_landscape.json"
    fi
elif [ "$workspace_shape" == "landscape" ]; then
    if [ "$workspace_size" -lt 2000 ]; then
        i3-msg append_layout "$HOME/.config/i3/code_1080_landscape.json"
    else
        i3-msg append_layout "$HOME/.config/i3/code_2560_landscape.json"
    fi
fi

# Run applications to fill layout
if [ -d "$1" ]; then
    i3-msg "exec rox-filer '$1'; exec urxvt -cd '$1'; exec urxvt -cd '$1'; exec gvim '$1'; exec gvim '$1'"
    if [ "$workspace_size" -gt 2000 ]; then
        # extra filer/editor for larger screens
        i3-msg "exec rox-filer '$1'; exec gvim '$1'"
    fi
else
    i3-msg "exec rox-filer; exec urxvt; exec urxvt; exec gvim ./; exec gvim ./"
    if [ "$workspace_size" -gt 2000 ]; then
        # extra filer/editor for larger screens
        i3-msg "exec rox-filer; exec gvim ./"
    fi
fi

Code layouts

Listing 9: ~/.config/i3/code_1080_landscape.json
// vim:ts=4:sw=4:et
// Code layout for 1920x1080p landscape displays
{
    // splith split container with 3 children
    "border": "normal",
    "floating": "auto_off",
    "layout": "splith",
    "percent": 1,
    "type": "con",
    "nodes": [
        {
            // splitv split container with 3 children
            "border": "normal",
            "floating": "auto_off",
            "layout": "splitv",
            "percent": 0.25,
            "type": "con",
            "nodes": [
                {
                    "border": "normal",
                    "current_border_width": 2,
                    "floating": "auto_off",
                    "geometry": {
                       "height": 282,
                       "width": 400,
                       "x": 0,
                       "y": 0
                    },
                    "percent": 0.25,
                    "swallows": [
                       {
                       "class": "^Rox\\-filer$",
                       "instance": "^rox\\-filer$"
                       }
                    ],
                    "type": "con"
                },
                {
                    "border": "normal",
                    "current_border_width": 2,
                    "floating": "auto_off",
                    "geometry": {
                       "height": 316,
                       "width": 400,
                       "x": 0,
                       "y": 0
                    },
                    "percent": 0.25,
                    "swallows": [
                       {
                       "class": "^URxvt$",
                       "instance": "^urxvt$"
                       }
                    ],
                    "type": "con"
                },
                {
                    "border": "normal",
                    "current_border_width": 2,
                    "floating": "auto_off",
                    "geometry": {
                       "height": 316,
                       "width": 400,
                       "x": 0,
                       "y": 0
                    },
                    "percent": 0.25,
                    "swallows": [
                       {
                       "class": "^URxvt$",
                       "instance": "^urxvt$"
                       }
                    ],
                    "type": "con"
                }
            ]
        },
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "geometry": {
               "height": 316,
               "width": 754,
               "x": 0,
               "y": 0
            },
            "percent": 0.375,
            "swallows": [
               {
               "class": "^Gvim$",
               "instance": "^gvim$"
               }
            ],
            "type": "con"
        },
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "geometry": {
               "height": 316,
               "width": 754,
               "x": 0,
               "y": 0
            },
            "percent": 0.375,
            "swallows": [
               {
               "class": "^Gvim$",
               "instance": "^gvim$"
               }
            ],
            "type": "con"
        }
    ]
}
Listing 10: ~/.config/i3/code_2560_landscape.json
// vim:ts=4:sw=4:et
// Code layout for 2560x1440 landscape displays
{
    // splith split container with 4 children
    "border": "normal",
    "floating": "auto_off",
    "layout": "splith",
    "percent": 1,
    "type": "con",
    "nodes": [
        {
            // splitv split container with 4 children
            "border": "normal",
            "floating": "auto_off",
            "layout": "splitv",
            "percent": 0.25,
            "type": "con",
            "nodes": [
                {
                    "border": "normal",
                    "current_border_width": 2,
                    "floating": "auto_off",
                    "geometry": {
                       "height": 282,
                       "width": 564,
                       "x": 0,
                       "y": 0
                    },
                    "percent": 0.25,
                    "swallows": [
                       {
                       "class": "^Rox\\-filer$",
                       "instance": "^rox\\-filer$"
                       }
                    ],
                    "type": "con"
                },
                {
                    "border": "normal",
                    "current_border_width": 2,
                    "floating": "auto_off",
                    "geometry": {
                       "height": 146,
                       "width": 304,
                       "x": 0,
                       "y": 0
                    },
                    "percent": 0.25,
                    "swallows": [
                       {
                       "class": "^Rox\\-filer$",
                       "instance": "^rox\\-filer$"
                       }
                    ],
                    "type": "con"
                },
                {
                    "border": "normal",
                    "current_border_width": 2,
                    "floating": "auto_off",
                    "geometry": {
                       "height": 316,
                       "width": 576,
                       "x": 0,
                       "y": 0
                    },
                    "percent": 0.25,
                    "swallows": [
                       {
                       "class": "^URxvt$",
                       "instance": "^urxvt$"
                       }
                    ],
                    "type": "con"
                },
                {
                    "border": "normal",
                    "current_border_width": 2,
                    "floating": "auto_off",
                    "geometry": {
                       "height": 316,
                       "width": 576,
                       "x": 0,
                       "y": 0
                    },
                    "percent": 0.25,
                    "swallows": [
                       {
                       "class": "^URxvt$",
                       "instance": "^urxvt$"
                       }
                    ],
                    "type": "con"
                }
            ]
        },
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "geometry": {
               "height": 316,
               "width": 576,
               "x": 0,
               "y": 0
            },
            "percent": 0.25,
            "swallows": [
               {
               "class": "^Gvim$",
               "instance": "^gvim$"
               }
            ],
            "type": "con"
        },
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "geometry": {
               "height": 316,
               "width": 576,
               "x": 0,
               "y": 0
            },
            "percent": 0.25,
            "swallows": [
               {
               "class": "^Gvim$",
               "instance": "^gvim$"
               }
            ],
            "type": "con"
        },
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "geometry": {
               "height": 316,
               "width": 576,
               "x": 0,
               "y": 0
            },
            "percent": 0.25,
            "swallows": [
               {
               "class": "^Gvim$",
               "instance": "^gvim$"
               }
            ],
            "type": "con"
        }
    ]
}

Documentation workspace

Listing 11: ~/.config/i3/ws_new_doc.sh
#!/bin/bash

workspace_shape=$(i3-msg -t get_workspaces|jq -r '.[]|select (.focused==true)|if .rect.width >= .rect.height then "landscape" else "portrait" end')

# Select next highest unused number
workspace_number=$(($(i3-msg -t get_workspaces |jq -r '.[].name' \
    |cut -d':' -f 1 |sort -n| tail -1) + 1))

# Switch to and rename workspace
i3-msg workspace number $workspace_number
i3-msg rename workspace to "$workspace_number:doc"

# Load appropriate layout
if [ "$workspace_shape" == "portrait" ]; then
    i3-msg append_layout "$HOME/.config/i3/doc-portrait.json"
elif [ "$workspace_shape" == "landscape" ]; then
    i3-msg append_layout "$HOME/.config/i3/doc-landscape.json"
fi

# Run applications to fill layout
i3-msg exec rox-filer "$HOME/documents"

Documentation layouts

Listing 12: ~/.config/i3/doc-portrait.json
// vim:ts=4:sw=4:et
{
    // splith split container with 1 children
    "border": "normal",
    "floating": "auto_off",
    "layout": "splith",
    "percent": 0.294520547945205,
    "type": "con",
    "nodes": [
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "geometry": {
               "height": 304,
               "width": 657,
               "x": 0,
               "y": 0
            },
            "percent": 1,
            "swallows": [
               {
                "class": "^Rox\\-filer$",
                "instance": "^rox\\-filer$"
               }
            ],
            "type": "con"
        }
    ]
}

{
    // tabbed split container
    "border": "normal",
    "floating": "auto_off",
    "layout": "tabbed",
    "percent": 0.705479452054795,
    "type": "con",
    "swallows": [
        {
               "class": "^Atril$|^Devhelp$|^Evince$|^Gvim$|^calibre$",
               "instance": "^atril$|^calibre\\-ebook\\-viewer$|^devhelp$|^evince$|^gvim$"
        }
    ],
    "nodes": [
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "percent": 0.5,
            "swallows": [
                {
                   "class": "^Atril$|^Devhelp$|^Evince$|^Gvim$|^calibre$",
                   "instance": "^atril$|^calibre\\-ebook\\-viewer$|^devhelp$|^evince$|^gvim$"
                }
            ],
            "type": "con",
            "marks": [
                "swallowed1"
            ]
        },
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "percent": 0.5,
            "swallows": [
                {
                   "class": "^Atril$|^Devhelp$|^Evince$|^Gvim$|^calibre$",
                   "instance": "^atril$|^calibre\\-ebook\\-viewer$|^devhelp$|^evince$|^gvim$"
                }
            ],
            "type": "con",
            "marks": [
                "swallowed1"
            ]
        }
    ],
    "mark": "doc"
}
Listing 13: ~/.config/i3/doc-landscape.json
// vim:ts=4:sw=4:et
{
    "border": "normal",
    "current_border_width": 2,
    "floating": "auto_off",
    "geometry": {
       "height": 480,
       "width": 657,
       "x": 0,
       "y": 0
    },
    "percent": 0.22,
    "swallows": [
       {
        "class": "^Rox\\-filer$",
        "instance": "^rox\\-filer$"
       }
    ],
    "type": "con"
}

{
    // tabbed split container with 2 children
    "border": "normal",
    "floating": "auto_off",
    "layout": "tabbed",
    "percent": 0.39,
    "type": "con",
    "nodes": [
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "percent": 0.5,
            "swallows": [
               {
               "class": "^Atril$|^Devhelp$|^Evince$|^Gvim$|^calibre$",
               "instance": "^atril$|^calibre\\-ebook\\-viewer$|^devhelp$|^evince$|^gvim$"
               }
            ],
            "type": "con",
            "marks": [
                "swallowed1"
            ]
        },
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "percent": 0.5,
            "swallows": [
               {
               "class": "^Atril$|^Devhelp$|^Evince$|^Gvim$|^calibre$",
               "instance": "^atril$|^calibre\\-ebook\\-viewer$|^devhelp$|^evince$|^gvim$"
               }
            ],
            "type": "con",
            "marks": [
                "swallowed2"
            ]
        }
    ],
    "mark": "doc2"
}

{
    // tabbed split container with 2 children
    "border": "normal",
    "floating": "auto_off",
    "layout": "tabbed",
    "percent": 0.39,
    "type": "con",
    "nodes": [
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "percent": 0.5,
            "swallows": [
               {
               "class": "^Atril$|^Devhelp$|^Evince$|^Gvim$|^calibre$",
               "instance": "^atril$|^calibre\\-ebook\\-viewer$|^devhelp$|^evince$|^gvim$"
               }
            ],
            "type": "con",
            "marks": [
                "swallowed3"
            ]
        },
        {
            "border": "normal",
            "current_border_width": 2,
            "floating": "auto_off",
            "percent": 0.5,
            "swallows": [
               {
               "class": "^Atril$|^Devhelp$|^Evince$|^Gvim$|^calibre$",
               "instance": "^atril$|^calibre\\-ebook\\-viewer$|^devhelp$|^evince$|^gvim$"
               }
            ],
            "type": "con",
            "marks": [
                "swallowed4"
            ]
        }
    ],
    "mark": "doc"
}

Screen layout (autorandr)

Screen layout relies on autorandr and a script to select from detected layouts.

The script uses zenity to display simple dialog with all detected inputs. This is because complexity exploded with 4 displays, and it was no longer feasible to remember or fit all the screen configurations on a reasonable set of binds.

#!/bin/bash

autorandr --match-edid --force --load "$(autorandr --match-edid --detected | sed 's/ - /\n/'| zenity --list --print-column=ALL --title "Which layout?" --column "Description" --column "Layout" --separator=" - " --height 300 --width 200)"

The autorandr postswitch scripts still rely on my older support scripts for some things. These get output names from monitor serial numbers since outputs naming isn't idempotent.

Listing 14: ~/.screenlayout/screen_names.sh
#!/bin/sh

# for i in /sys/class/graphics/fb0/device/drm/card0/*/edid; do edid-decode < $i; done|grep Product

u2515h_serial="123456789"
u2515h=$(~/.screenlayout/serial2output.sh "$u2515h_serial")
export u2515h

u2414h_one_serial="123456789"
u2414h_one=$(~/.screenlayout/serial2output.sh "$u2414h_one_serial")
export u2414h_one

magedok_15_serial="123456789"
magedok_15=$(~/.screenlayout/serial2output.sh "$magedok_15_serial")
export magedok_15

magedok_9_serial="123456789"
magedok_9=$(~/.screenlayout/serial2output.sh "$magedok_9_serial")
export magedok_9

# not currently used displays

u2414h_two_serial="123456789"
u2414h_two=$(~/.screenlayout/serial2output.sh "$u2414h_two_serial")
export u2414h_two

# one of two 11.6 1080p, probably the one with the vesa mount screwholes
magedok_vesa_serial="123456789"
magedok_vesa=$(~/.screenlayout/serial2output.sh "$magedok_vesa_serial")
export magedok_vesa
Listing 15: ~/.screenlayout/serial2output.sh
#!/bin/bash

# this probably came from: https://stackoverflow.com/a/24933353
while read -r output hex; do
    serial=$(echo "$hex" | xxd -r -p -)
    if [ "$1" = "$serial" ]; then
        echo "$output"
        exit
    fi
done < <(xrandr --prop | awk '
    !/^[ \t]/ {
        if (output && hex) print output, hex
        output=$1
        hex=""
    }
    /[:.]/ && h {
        sub(/.*000000ff00/, "", hex)
        hex = substr(hex, 0, 26) "0a"
        sub(/0a.*/, "", hex)
        h=0
    }
    h {sub(/[ \t]+/, ""); hex = hex $0}
    /EDID.*:/ {h=1}
    END {if (output && hex) print output, hex}
    ' | sort
)

autorandr postswitch scripts

Listing 16: ~/.config/autorandr/postswitch.d/3-restart_i3_fix_wallpaper
#!/bin/sh

sleep 2

i3-msg restart
feh --no-fehbg --bg-fill "$(cat ~/.last_fehbg)"
Listing 17: ~/.config/autorandr/postswitch.d/7-assign_touch_inputs
#!/bin/sh

sleep 6

. ~/.screenlayout/screen_names.sh

if xrandr --listactivemonitors | grep -q "$magedok_9"; then
    xinput enable "ILITEK ILITEK-TP"
    xinput map-to-output "ILITEK ILITEK-TP" "$magedok_9"
else
    xinput disable "ILITEK ILITEK-TP"
fi

if xrandr --listactivemonitors | grep -q "$magedok_15"; then
    xinput enable "G2Touch Multi-Touch by G2TSP"
    xinput map-to-output "G2Touch Multi-Touch by G2TSP" "$magedok_15"
else
    xinput disable "G2Touch Multi-Touch by G2TSP"
fi

if xrandr --listactivemonitors | grep -q "$magedok_vesa"; then
    xinput enable "N-trig DuoSense"
    xinput enable "N-trig DuoSense Mouse"
    xinput enable "N-trig DuoSense Stylus"
    xinput map-to-output "N-trig DuoSense" "$magedok_vesa"
    xinput map-to-output "N-trig DuoSense Mouse" "$magedok_vesa"
    xinput map-to-output "N-trig DuoSense Stylus" "$magedok_vesa"
else
    xinput disable "N-trig DuoSense"
    xinput disable "N-trig DuoSense Mouse"
    xinput disable "N-trig DuoSense Stylus"
fi

Move workspaces script

Script to move all workspaces to a particular display (excluding workspace 1). This is because I find taking away displays from i3 isn't always predictable. If I pre-move workspaces then I can disable monitors without annoying hiccups.

This script prompts the user for the target output using zenity.

Listing 18: ~/.config/i3/mv_ws.sh
#!/bin/bash

if output=$(xrandr --current --query|grep " connected "|cut -d ' ' -f 1 | zenity --list --title "Which output?" --column "Outputs"); then
    while read -r name; do
        i3-msg workspace "${name}"
        i3-msg move workspace to output "$output"
    done< <(i3-msg -t get_workspaces | jq -r '.[]|select (.num>1) | "\(.name)"')
fi

Disable input device scripts

These are for the Thinkpad X280. Just simple toggles for the touchpad and touchscreen when I don't want them.

Listing 19: ~/.config/i3/toggle_tp.sh
#!/bin/bash

declare -i ID
ID=`xinput list | grep -Eo 'Synaptics TM3381-002\s*id\=[0-9]{1,2}' | grep -Eo 'id\=[0-9]{1,2}'|grep -Eo '[0-9]{1,2}'`
declare -i STATE
STATE=`xinput list-props $ID|grep 'Device Enabled'|awk '{print $4}'`
if [ $STATE -eq 1 ]
then
    xinput disable $ID
    echo "Touchpad disabled."
else
    xinput enable $ID
    echo "Touchpad enabled."
fi
Listing 20: ~/.config/i3/toggle_ts.sh
#!/bin/bash

declare -i ID
ID=`xinput list | grep -Eo 'Touchscreen\s*id\=[0-9]{1,2}' | grep -Eo 'id\=[0-9]{1,2}'|grep -Eo '[0-9]{1,2}'`
declare -i STATE
STATE=`xinput list-props $ID|grep 'Device Enabled'|awk '{print $4}'`
if [ $STATE -eq 1 ]
then
    xinput disable $ID
    echo "Touchscreen disabled."
else
    xinput enable $ID
    echo "Touchscreen enabled."
fi