Wayland / Sway wmfocus alternative

Switching to Sway on Wayland: A Custom Window Focus Solution

Posted on Nov. 8, 2024 in linux-desktop, sway, i3.

After a long period of procrastination, I decided to start using Wayland instead of X11.

While Sway promises i3 compatibility, it is not 100% compatible, and some tools I used with i3 are not working. One such tool is wmfocus, which allows rapid focusing on a specific window. It displays a letter for each window in the workspace, so you can easily switch to it by pressing the corresponding letter. This is especially useful when you have many windows open, particularly on multiple screens.

Screenshoot:

2024-11-11_15-55

Unfortunately, wmfocus does not work with Wayland windows, and there is an open issue about it (GitHub issue). While waiting for the issue to be resolved, I looked for alternatives and ways to implement similar functionality.

I gathered some ideas from Reddit discussions and put together a solution that works for me.

The idea is to assign a mark to each window and switch to that window using its assigned letter. This requires the title bar to be visible for all windows. It’s not as elegant a solution as wmfocus, but it gets the job done.

marks

How to implement it

1. In your Sway config file, configure the title bar to be visible for all windows:

# enable title bar
default_border normal

2. Create mark.sh script:

#!/bin/bash

if ! command -v jq &> /dev/null; then
  echo "The 'jq' command is required but not installed. Please visit https://jqlang.github.io/jq/download/ to download and install it."
  exit 1
fi

# Get a list of existing marks
existing_marks=$(swaymsg -t get_tree | jq -r '.. | select(.type? == "con" and .marks != null) | .marks[]')

# Define the list of possible alphanumeric marks (5-9, a-z, A-Z)
possible_marks=({5..9} {a..z} {A..Z})

current_mark=$(swaymsg -t get_tree | jq -r '.. | select(.focused? == true) | .marks[]?')
# skip if window already have a mark
if ! [ $current_mark ]; then
	# Find the first unused mark
	for mark in "${possible_marks[@]}"; do
		if ! grep -qw "$mark" <<<"$existing_marks"; then
			# Assign the unused mark to the currently focused window
			swaymsg mark "$mark"
			exit 0
		fi
	done
	# If all marks are used, print a message (optional)
	# echo "No available marks left"
	exit 1
fi

Make the script executable:

chmod 755 mark.sh

Now you can test the script. Open a terminal window and execute mark.sh in it. A new mark should be added and visible in the title bar.

3. Update the Sway config file to run the script for every window and add a mode for choosing a mark

# run ~/mark.sh script for every window
for_window [all] exec --no-startup-id PATH_WITH_MARK_SCRIPT/mark.sh
# add mode for choosing a mark
set $goto_mark "Go to mark?" 
bindsym $mod+i mode $goto_mark
mode $goto_mark {
    # Numeric marks 0-9
    bindsym 1 [con_mark="1"] focus, mode "default"
    bindsym 2 [con_mark="2"] focus, mode "default"
    bindsym 3 [con_mark="3"] focus, mode "default"
    bindsym 4 [con_mark="4"] focus, mode "default"
    bindsym 5 [con_mark="5"] focus, mode "default"
    bindsym 6 [con_mark="6"] focus, mode "default"
    bindsym 7 [con_mark="7"] focus, mode "default"
    bindsym 8 [con_mark="8"] focus, mode "default"
    bindsym 9 [con_mark="9"] focus, mode "default"
    bindsym 0 [con_mark="0"] focus, mode "default"

    # Lowercase marks a-z
    bindsym a [con_mark="a"] focus, mode "default"
    bindsym b [con_mark="b"] focus, mode "default"
    bindsym c [con_mark="c"] focus, mode "default"
    bindsym d [con_mark="d"] focus, mode "default"
    bindsym e [con_mark="e"] focus, mode "default"
    bindsym f [con_mark="f"] focus, mode "default"
    bindsym g [con_mark="g"] focus, mode "default"
    bindsym h [con_mark="h"] focus, mode "default"
    bindsym i [con_mark="i"] focus, mode "default"
    bindsym j [con_mark="j"] focus, mode "default"
    bindsym k [con_mark="k"] focus, mode "default"
    bindsym l [con_mark="l"] focus, mode "default"
    bindsym m [con_mark="m"] focus, mode "default"
    bindsym n [con_mark="n"] focus, mode "default"
    bindsym o [con_mark="o"] focus, mode "default"
    bindsym p [con_mark="p"] focus, mode "default"
    bindsym q [con_mark="q"] focus, mode "default"
    bindsym r [con_mark="r"] focus, mode "default"
    bindsym s [con_mark="s"] focus, mode "default"
    bindsym t [con_mark="t"] focus, mode "default"
    bindsym u [con_mark="u"] focus, mode "default"
    bindsym v [con_mark="v"] focus, mode "default"
    bindsym w [con_mark="w"] focus, mode "default"
    bindsym x [con_mark="x"] focus, mode "default"
    bindsym y [con_mark="y"] focus, mode "default"
    bindsym z [con_mark="z"] focus, mode "default"

    # Uppercase marks A-Z
    bindsym Shift+A [con_mark="A"] focus, mode "default"
    bindsym Shift+B [con_mark="B"] focus, mode "default"
    bindsym Shift+C [con_mark="C"] focus, mode "default"
    bindsym Shift+D [con_mark="D"] focus, mode "default"
    bindsym Shift+E [con_mark="E"] focus, mode "default"
    bindsym Shift+F [con_mark="F"] focus, mode "default"
    bindsym Shift+G [con_mark="G"] focus, mode "default"
    bindsym Shift+H [con_mark="H"] focus, mode "default"
    bindsym Shift+I [con_mark="I"] focus, mode "default"
    bindsym Shift+J [con_mark="J"] focus, mode "default"
    bindsym Shift+K [con_mark="K"] focus, mode "default"
    bindsym Shift+L [con_mark="L"] focus, mode "default"
    bindsym Shift+M [con_mark="M"] focus, mode "default"
    bindsym Shift+N [con_mark="N"] focus, mode "default"
    bindsym Shift+O [con_mark="O"] focus, mode "default"
    bindsym Shift+P [con_mark="P"] focus, mode "default"
    bindsym Shift+Q [con_mark="Q"] focus, mode "default"
    bindsym Shift+R [con_mark="R"] focus, mode "default"
    bindsym Shift+S [con_mark="S"] focus, mode "default"
    bindsym Shift+T [con_mark="T"] focus, mode "default"
    bindsym Shift+U [con_mark="U"] focus, mode "default"
    bindsym Shift+V [con_mark="V"] focus, mode "default"
    bindsym Shift+W [con_mark="W"] focus, mode "default"
    bindsym Shift+X [con_mark="X"] focus, mode "default"
    bindsym Shift+Y [con_mark="Y"] focus, mode "default"
    bindsym Shift+Z [con_mark="Z"] focus, mode "default"

    # Return to default mode
    bindsym Return mode "default"
    bindsym Escape mode "default"
}

Replace PATH_WITH_MARK_SCRIPT with the path where you saved the mark.sh script.

4. Reload the Sway config and test the script by pressing mod+i, then a letter to switch to the window with that mark.


Notes

The core of this solution is to use a custom script, mark.sh, to assign a mark to each window as it opens. The mark.sh script uses the jq utility to parse the JSON output of the swaymsg -t get_tree command.

The goal is to switch to a window with the minimum number of key presses. Although i3-input would be ideal for this, it unfortunately lacks a replacement in Sway (see GitHub issue). I tried Zenity, Rofi, and other dialog/menu utilities, but all of them require pressing Enter to complete the selection, which doesn’t match the ergonomics I wanted.

Later, I found that I could use binding modes to achieve this.

Share on Reddit