i3 Desktop Part 6: Hardware Menus
Hardware controls are where a minimal desktop can become awkward quickly.
Changing Wi-Fi from a terminal is fine occasionally. Doing it every day is not. Switching audio outputs with raw commands is possible. Remembering the exact command when headphones fail to become default is a waste of attention. Monitor layout is even worse because the command is always slightly different depending on what is plugged in.
This setup uses small menus around the tools I would use manually anyway.
That means:
xrandrfor monitor layout and scalingsetxkbmapand IBus sync for keyboard layoutnmclifor networkbluetoothctlfor Bluetoothwpctlfor PipeWire audio
The menus do not hide the system. They make the common path quick.
Monitor layout
.local/bin/i3-monitor-layout handles display arrangements.
It supports:
- auto or docked
- laptop only
- external only
- extend right
- mirror
- list outputs
- open display scaling
The auto layout is the important one. On startup, i3 runs i3-monitor-layout auto --quiet. If an external display is connected, it makes that the primary output and keeps the internal display to the left. If no external display is connected, it falls back to laptop only.
That is enough for the two states I use most: undocked and docked.
The script also reapplies the wallpaper after changing layout. That sounds minor, but it avoids the half-broken look that can happen after xrandr rearranges outputs.
Display scaling
.local/bin/i3-monitor-scaling is separate because layout and scale are different problems.
It can apply scaling to all active displays or a selected output. The menu offers common UI sizes: 100, 125, 150, 175, 200 percent, plus smaller UI options and a custom percent.
Internally it converts UI percent into an xrandr scale value. For example, a larger 150 percent UI means an xrandr scale of 100 / 150.
It also has a status view that reads xrandr’s transform output and reports the current UI scale. This is useful because display scaling can otherwise become invisible state.
Keyboard layout
The keyboard layout script exists because I move between the laptop keyboard and an external keyboard.
.local/bin/i3-keyboard-layout switches between:
- UK English for the laptop
- US English for the external keyboard
The selected layout is saved in .config/i3/keyboard-layout and reapplied on i3 reload. It also syncs IBus where possible and hides the IBus tray indicator.
This is a small thing, but it removes a constant irritation. The keyboard layout becomes an explicit desktop state rather than something I manually fix after noticing a quote key is wrong.
Network
The network menu wraps nmcli.
It can show status, connect to Wi-Fi, connect to saved Wi-Fi, disconnect Wi-Fi, toggle Wi-Fi, open the NetworkManager connection editor, and toggle networking as a whole.
For new Wi-Fi connections, it scans, shows SSIDs with signal and security, prompts for a password through rofi when needed, and then calls nmcli device wifi connect.
The status bar uses the same underlying idea. It finds the primary connected device, filters out loopback, bridges, veth devices, Docker-style interfaces, and similar noise, then shows the connection name and IP address. If a VPN or WireGuard connection is active, it appends VPN.
Bluetooth
The Bluetooth menu wraps bluetoothctl.
It can show controller status, power Bluetooth on or off, connect paired devices, disconnect devices, scan, pair scanned devices, make the adapter pairable and discoverable, or open blueman-manager if I need the full GUI.
Pairing is one of those flows where I do not want to remember the exact sequence every time. The script powers on Bluetooth, starts the agent, scans for ten seconds, lets me pick a device, pairs, trusts, and connects.
That is a good use of a script. It captures a workflow, not a new abstraction.
Audio
Audio uses PipeWire through wpctl.
The menu can switch output devices, switch input devices, mute output, mute input, adjust volume, open pavucontrol, and show current status.
The status script parses wpctl status to list sinks and sources, marking the current default device. Selecting a device calls wpctl set-default. Volume keys call wpctl set-volume, and the volume block in the bar refreshes immediately.
This keeps audio control keyboard-first but still gives me a fallback path when devices appear with unhelpful names.
The pattern
These scripts are not large, and that is the point.
Each one wraps an existing Linux command-line tool with:
- a rofi menu
- a direct command mode for keybindings
- dunst feedback
- a status action
- a small amount of state when needed
That is enough to make hardware controls feel integrated into i3 without depending on a full desktop settings daemon.