Lightweight window tiling and desktop startup profile management for X11 using wmctrl, xdotool, and zenity.
wmctrl— window manipulation (move, resize, workspace)xdotool— display geometry, active window ID, window infozenity— profile selection dialog (startup only)
git clone <repo> ~/.local/binEnsure ~/.local/bin is on PATH. Scripts reference each other via relative paths from dirname $0.
Run any script to position the active window:
| Command | Effect |
|---|---|
quarter/quarter-top-left |
Top-left quadrant |
quarter/quarter-top-right |
Top-right quadrant |
quarter/quarter-bottom-left |
Bottom-left quadrant |
quarter/quarter-bottom-right |
Bottom-right quadrant |
quarter/quarter-center |
Centered quarter |
one/one-half-left |
Left half |
one/one-half-right |
Right half |
one/one-third-left |
Left third |
one/one-third-right |
Right third |
one/one-fifth-left |
Left fifth |
one/one-fifth-right |
Right fifth |
three/three-quarters-left |
Left three-quarters |
three/three-quarters-right |
Right three-quarters |
four/four-fifths-left |
Left four-fifths |
four/four-fifths-right |
Right four-fifths |
Nudge active window dimensions:
| Command | Effect |
|---|---|
increase/increase-width |
Widen by W_STEP (100px) |
increase/increase-height |
Heighten by H_STEP (50px) |
decrease/decrease-width |
Narrow by W_STEP |
decrease/decrease-height |
Shorten by H_STEP |
./startup-profile-switcher.shPresents a zenity list dialog with three profiles:
- Development — launches Firefox, terminal, Zed, Obsidian; arranges across workspaces
- Personal — personal workspace apps
- Minimal — minimal session
Default fallback is Development.
~/.local/bin/
├── tiling-common.sh # Shared screen dims, fractions, helpers
├── startup-profile-switcher.sh # Zenity profile picker
├── quarter/ # 5 scripts: 4 quadrants + center
├── one/ # 6 scripts: 1/2, 1/3, 1/5 left/right
├── three/ # 2 scripts: 3/4 left/right
├── four/ # 2 scripts: 4/5 left/right
├── increase/ # 2 scripts: increase width/height
├── decrease/ # 2 scripts: decrease width/height
└── startup/
├── startup-helpers.sh # Sourced helpers: get_wid, place_window, place
├── startup-dev.sh # Development profile
├── startup-personal.sh # Personal profile
└── startup-minimal.sh # Minimal profile
Shared variables and helper functions. Sourced by all tiling scripts.
Geometry fractions (derived from xdotool getdisplaygeometry):
| Variable | Value |
|---|---|
SW, SH |
Screen width, height |
W_1_2 |
SW / 2 |
W_1_3 |
SW / 3 |
W_1_4 |
SW / 4 |
W_1_5 |
SW / 5 |
W_2_3 |
SW * 2 / 3 |
W_3_4 |
SW * 3 / 4 |
W_4_5 |
SW * 4 / 5 |
H_1_2 |
SH / 2 |
H_1_4 |
SH / 4 |
W_STEP |
Width increment (100px) |
H_STEP |
Height increment (50px) |
Helper functions:
get_active_geometry— resolves active window decimal ID viaxdotool getactivewindow, converts to hex forwmctrl, outputshex_wid X Y WIDTH HEIGHTload_active_geometry— parsesget_active_geometryoutput into$WID $X $Y $WIDTH $HEIGHTclamp VALUE MIN MAX— constrains value within rangeapply_window_geometry WID W H [X] [Y]— callswmctrl -i -rwith bounds
All static tiling scripts follow this pattern:
#!/bin/bash
BIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
source "$BIN_ROOT/tiling-common.sh"
wmctrl -r :ACTIVE: -e 0,X,Y,WIDTH,HEIGHT#!/bin/bash
BIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
source "$BIN_ROOT/tiling-common.sh"
load_active_geometry
UPDATED_WIDTH=$(clamp $((WIDTH + W_STEP)) 1 $SW)
apply_window_geometry "$WID" "$UPDATED_WIDTH" "$HEIGHT"#!/bin/bash
source "$(dirname "$0")/../tiling-common.sh"
source "$(dirname "$0")/startup-helpers.sh"
app1 &
app2 &
place "app.class" X Y WIDTH HEIGHT WORKSPACEstartup-helpers.sh functions:
get_wid CLASS— pollswmctrl -l -xfor window by class (15s timeout, 1s intervals), returns hex window IDplace_window WID CLASS X Y W H WS— removes maximized state, moves/resizes, sends to workspaceplace CLASS X Y W H WS— convenience wrapper: callsget_widthenplace_window
Edit in tiling-common.sh:
W_STEP=100
H_STEP=50Copy an existing tiling script and change the wmctrl -e arguments.
- Create
startup/startup-<name>.sh - Source
tiling-common.shandstartup-helpers.sh - Launch apps with
& - Call
placefor each window - Add case in
startup-profile-switcher.sh
Bind these in your window manager or desktop environment:
| Shortcut | Command |
|---|---|
Super+Shift+Left |
quarter/quarter-top-left |
Super+Shift+Right |
quarter/quarter-top-right |
Super+Ctrl+Left |
one/one-half-left |
Super+Ctrl+Right |
one/one-half-right |
Super+Ctrl+Up |
increase/increase-height |
Super+Ctrl+Down |
decrease/decrease-height |
Super+Ctrl+Shift+Right |
increase/increase-width |
Super+Ctrl+Shift+Left |
decrease/decrease-width |
wmctrl -e accepts signed 16-bit x-offsets (max 32767). On wide/4K monitors, right-aligned positions that combine large offsets may overflow this limit. When SW > 1366, use fraction-based offsets (e.g., $W_1_2, $W_3_4). Avoid hardcoded values that could exceed 32767.
Install missing dependencies:
# Debian/Ubuntu
sudo apt install wmctrl xdotool zenity
# Arch
sudo pacman -S wmctrl xdotool zenity
# Fedora
sudo dnf install wmctrl xdotool zenityRemove maximized state first:
wmctrl -i -r "$wid" -b remove,maximized_vert,maximized_horizThis is handled automatically by place() in startup scripts.
Gap management is delegated to compositor window decorations (gtk.css styling). The ASIDE_GAP variable in tiling-common.sh is commented out — adjust your compositor shadow/decorations instead.
- Signed 16-bit x-offset limit:
wmctrl -ecaps x at 32767. Scripts using fraction variables are safe; watch for this when adding new positions on monitors wider than 1366px. - No tests, no CI, no build system: Add new scripts by copying and modifying an existing one.
- Shebang split: Most scripts use
#!/bin/bash; two use#!/usr/bin/env bash. Both are supported.