diff options
Diffstat (limited to 'bin')
| -rwxr-xr-x | bin/gnome-move-windows | 167 | ||||
| -rwxr-xr-x | bin/gnome-set-config | 79 | ||||
| -rwxr-xr-x | bin/gnome-window-switcher | 72 | ||||
| -rwxr-xr-x | bin/rofi | 30 | ||||
| -rwxr-xr-x | bin/setup-install-fonts | 26 |
5 files changed, 358 insertions, 16 deletions
diff --git a/bin/gnome-move-windows b/bin/gnome-move-windows index d6cd934..4611759 100755 --- a/bin/gnome-move-windows +++ b/bin/gnome-move-windows @@ -1,36 +1,175 @@ #!/bin/sh # Move windows according to my workflow. Check bin/gnome-set-config to -# see its key-binding. Needs wmctrl. +# see its key-binding. +# +# GNOME on Wayland does not expose native windows to wmctrl. Prefer the +# Workspace Router GNOME Shell extension, which runs inside Shell and can +# see both Wayland and Xwayland windows. Fall back to wmctrl for sessions +# where the extension is unavailable. + +workspace_router_call() { + method="$1" + + command -v gdbus >/dev/null 2>&1 || return 1 + + gdbus call \ + --session \ + --dest name.rbenencia.WorkspaceRouter \ + --object-path /name/rbenencia/WorkspaceRouter \ + --method "name.rbenencia.WorkspaceRouter.$method" +} + +is_x11_session() { + [ "${XDG_SESSION_TYPE:-}" = "x11" ] || { + [ -n "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ] + } +} + +is_wayland_session() { + [ "${XDG_SESSION_TYPE:-}" = "wayland" ] || [ -n "${WAYLAND_DISPLAY:-}" ] +} + +print_diagnostics() { + if workspace_router_call ListWindows >/tmp/gnome-move-windows-router.out 2>/tmp/gnome-move-windows-router.err; then + router_status="available" + router_preview=$(sed -n '1p' /tmp/gnome-move-windows-router.out) + else + router_status="unavailable" + router_preview=$(sed -n '1p' /tmp/gnome-move-windows-router.err) + fi + + if command -v wmctrl >/dev/null 2>&1; then + wmctrl_count=$(wmctrl -l 2>/dev/null | wc -l | awk '{print $1}') + else + wmctrl_count="not-installed" + fi + + echo "session_type=${XDG_SESSION_TYPE:-unknown}" + echo "display=${DISPLAY:-unset}" + echo "wayland_display=${WAYLAND_DISPLAY:-unset}" + echo "workspace_router=${router_status}" + echo "workspace_router_preview=${router_preview:-none}" + echo "wmctrl_window_count=${wmctrl_count}" + + if is_wayland_session && [ "$router_status" != "available" ]; then + echo "diagnosis=Wayland session without Workspace Router; fallback can only manage Xwayland windows" + fi +} + +warn_wayland_fallback() { + echo "gnome-move-windows: Workspace Router is unavailable on Wayland; falling back to wmctrl, which only sees some windows" >&2 + echo "gnome-move-windows: Run 'gnome-move-windows --diagnose' and ensure the workspace-router-cli GNOME extension is enabled" >&2 +} + +case "$1" in + --list) + workspace_router_call ListWindows + exit $? + ;; + --diagnose) + print_diagnostics + exit $? + ;; +esac + +if [ -z "$GNOME_MOVE_WINDOWS_FORCE_WMCTRL" ] && ! is_x11_session; then + if workspace_router_call RouteWindows >/dev/null 2>&1; then + exit 0 + fi + + if is_wayland_session; then + warn_wayland_fallback + fi +fi + +command -v wmctrl >/dev/null 2>&1 || { + echo "gnome-move-windows: Workspace Router is unavailable and wmctrl is not installed" >&2 + exit 1 +} # Move all windows to the primary display. If they're on the secondary # display, and we try to move them to a workspace, it won't work. -for window_id in $(wmctrl -l | awk '{print $1}'); do - wmctrl -i -r $window_id -e 0,0,0,-1,-1 -done +move_windows_to_primary() { + window_ids=$(wmctrl -l | awk '{print $1}') + maximized_windows="" + fullscreen_windows="" + has_xprop=0 + + command -v xprop >/dev/null 2>&1 && has_xprop=1 + + for window_id in $window_ids; do + if [ "$has_xprop" -eq 1 ]; then + state=$(xprop -id "$window_id" _NET_WM_STATE 2>/dev/null || true) + + case "$state" in + *"_NET_WM_STATE_MAXIMIZED_VERT"*"_NET_WM_STATE_MAXIMIZED_HORZ"*|*"_NET_WM_STATE_MAXIMIZED_HORZ"*"_NET_WM_STATE_MAXIMIZED_VERT"*) + maximized_windows="$maximized_windows $window_id" + ;; + esac + + case "$state" in + *"_NET_WM_STATE_FULLSCREEN"*) + fullscreen_windows="$fullscreen_windows $window_id" + ;; + esac + fi + + wmctrl -i -r "$window_id" -b remove,fullscreen + wmctrl -i -r "$window_id" -b remove,maximized_vert,maximized_horz + done + + sleep 0.2 + + for window_id in $window_ids; do + wmctrl -i -r "$window_id" -e 0,0,0,-1,-1 + done + + sleep 0.2 + + for window_id in $maximized_windows; do + wmctrl -i -r "$window_id" -b add,maximized_vert,maximized_horz + done + + for window_id in $fullscreen_windows; do + wmctrl -i -r "$window_id" -b add,fullscreen + done +} + +move_windows_to_primary # Assign windows to predetermined workplaces -misc=$(wmctrl -l | awk '/isco|eepa/ {print $1}') -main="$(wmctrl -l | awk '/ main$/ {print $1}')" -communications="$(wmctrl -l | awk '/Webex|Slack|communications/ {print $1}')" -terminals="$(wmctrl -l | awk '/Alacritty|terminals$/ {print $1}')" -browsers="$(wmctrl -l | awk '/Firefox|Chrom/ {print $1}')" +misc=$(wmctrl -xl | awk 'tolower($0) ~ /(com\\.cisco\\.secureclient|secure client|anyconnect|vpnui|keepass)/ {print $1}') +main="$(wmctrl -xl | awk 'tolower($0) ~ / emacs/ {print $1}')" +communications="$(wmctrl -xl | awk 'tolower($0) ~ /(webex|slack|communications|notmuch|outlook|elfeed|thunderbird)/ {print $1}')" +media="$(wmctrl -xl | awk 'tolower($0) ~ /(youtube|spotify)/ {print $1}')" +terminals="$(wmctrl -xl | awk 'tolower($0) ~ /(alacritty|kitty|terminal)/ {print $1}')" +teleport="$(wmctrl -xl | awk 'tolower($0) ~ /teleport/ {print $1}')" +browsers="$(wmctrl -xl | awk 'tolower($0) ~ /(firefox|chrom)/ {print $1}')" for window_id in $misc; do - wmctrl -i -r $window_id -t 4 + wmctrl -i -r "$window_id" -t 4 done for window_id in $main; do - wmctrl -i -r $window_id -t 0 + wmctrl -i -r "$window_id" -t 0 done for window_id in $browsers; do - wmctrl -i -r $window_id -t 1 + wmctrl -i -r "$window_id" -t 1 done for window_id in $communications; do - wmctrl -i -r $window_id -t 2 + wmctrl -i -r "$window_id" -t 2 done for window_id in $terminals; do - wmctrl -i -r $window_id -t 3 + wmctrl -i -r "$window_id" -t 3 +done + +for window_id in $teleport; do + wmctrl -i -r "$window_id" -t 5 +done + +for window_id in $media; do + wmctrl -i -r "$window_id" -t 8 done diff --git a/bin/gnome-set-config b/bin/gnome-set-config index 84c3319..0a05ad7 100755 --- a/bin/gnome-set-config +++ b/bin/gnome-set-config @@ -4,10 +4,76 @@ # for e in $(gsettings list-schemas | grep bind); do gsettings list-recursively $e; done NUM_WORKSPACES=9 +WORKSPACE_ROUTER_UUID=workspace-router@rbenencia.name +WORKSPACE_ROUTER_CLI_UUID=workspace-router-cli@rbenencia.name +ORG_AGENDA_INDICATOR_UUID=org-agenda-indicator@rbenencia.name + +ensure_extension_enabled() { + uuid="$1" + extension_dir="" + + for candidate in \ + "$HOME/.local/share/gnome-shell/extensions/$uuid" \ + "$HOME/.local/gnome-shell/extensions/$uuid"; do + if [ -d "$candidate" ]; then + extension_dir="$candidate" + break + fi + done + + if [ -z "$extension_dir" ]; then + return 0 + fi + + if [ -d "$extension_dir/schemas" ] && command -v glib-compile-schemas >/dev/null 2>&1; then + glib-compile-schemas "$extension_dir/schemas" >/dev/null 2>&1 || true + fi + + enabled_extensions=$( + python3 - "$uuid" "$(gsettings get org.gnome.shell enabled-extensions)" <<'PY' +import ast +import sys + +uuid = sys.argv[1] +enabled_extensions = ast.literal_eval(sys.argv[2]) + +if uuid not in enabled_extensions: + enabled_extensions.append(uuid) + +print(enabled_extensions) +PY + ) + gsettings set org.gnome.shell enabled-extensions "$enabled_extensions" + + if command -v gnome-extensions >/dev/null 2>&1; then + gnome-extensions enable "$uuid" >/dev/null 2>&1 || true + fi +} + +reload_extension() { + uuid="$1" + + if ! command -v gnome-extensions >/dev/null 2>&1; then + return 0 + fi + + gnome-extensions disable "$uuid" >/dev/null 2>&1 || true + gnome-extensions enable "$uuid" >/dev/null 2>&1 || true +} gsettings set org.gnome.mutter dynamic-workspaces false gsettings set org.gnome.desktop.wm.preferences num-workspaces $NUM_WORKSPACES +ensure_extension_enabled "$WORKSPACE_ROUTER_UUID" +ensure_extension_enabled "$WORKSPACE_ROUTER_CLI_UUID" +ensure_extension_enabled "$ORG_AGENDA_INDICATOR_UUID" +reload_extension "$WORKSPACE_ROUTER_CLI_UUID" + +ROFI_CMD="$HOME/bin/rofi" + +# Disable the default <Super>p. I don't use it, and it's disruptive when I accidentally trigger it. +gsettings set org.gnome.mutter.keybindings switch-monitor '[]' + for i in $(seq 1 $NUM_WORKSPACES); do gsettings set org.gnome.shell.keybindings switch-to-application-$i '[]' gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-$i "['<Super>$i']" @@ -15,7 +81,7 @@ for i in $(seq 1 $NUM_WORKSPACES); do done # This configuration is not present in gsettings; we need to fall back to dconf -bindings="emacs org-mode move-windows" +bindings="emacs org-mode move-windows rofi-run" keybindings_key="/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings" keybindings=$(echo $bindings | awk -v key="$keybindings_key" '{for(i=1;i<=NF;i++) printf("'\''" key "/" $i "/'\''%s", (i==NF ? "" : ","))}') keybindings="[$keybindings]" @@ -31,5 +97,14 @@ dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/org dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/org-mode/name "'org-capture'" dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/move-windows/binding "'<Shift><Super>m'" -dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/move-windows/command "'gnome-move-windows'" +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/move-windows/command "'$HOME/bin/gnome-move-windows'" dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/move-windows/name "'move-windows'" + +# Disable default for input-source switching. Leave <Super>space available +# for the GNOME Switcher extension. +gsettings set org.gnome.desktop.wm.keybindings switch-input-source "[]" +gsettings set org.gnome.desktop.wm.keybindings switch-input-source-backward '[]' + +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/rofi-run/binding "'<Super>f2'" +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/rofi-run/command "'$ROFI_CMD -show run'" +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/rofi-run/name "'rofi-run'" diff --git a/bin/gnome-window-switcher b/bin/gnome-window-switcher new file mode 100755 index 0000000..87cbcdc --- /dev/null +++ b/bin/gnome-window-switcher @@ -0,0 +1,72 @@ +#!/bin/sh + +workspace_router_call() { + method="$1" + shift + + command -v gdbus >/dev/null 2>&1 || return 1 + + gdbus call \ + --session \ + --dest name.rbenencia.WorkspaceRouter \ + --object-path /name/rbenencia/WorkspaceRouter \ + --method "name.rbenencia.WorkspaceRouter.$method" \ + "$@" +} + +list_windows_json() { + workspace_router_call ListWindows | + sed -n "s/^('\\(.*\\)',)\$/\\1/p" +} + +activate_window() { + window_id="$1" + workspace_router_call ActivateWindow "uint32:$window_id" >/dev/null +} + +command -v jq >/dev/null 2>&1 || { + echo "gnome-window-switcher: jq is required" >&2 + exit 1 +} + +command -v rofi >/dev/null 2>&1 || { + echo "gnome-window-switcher: rofi is required" >&2 + exit 1 +} + +windows_json=$(list_windows_json) || { + echo "gnome-window-switcher: Workspace Router is unavailable" >&2 + exit 1 +} + +choices=$( + printf '%s\n' "$windows_json" | + jq -r ' + sort_by(.workspace, .appName, .title) + | .[] + | [.id, ("[" + ((.workspace + 1) | tostring) + "]"), (.appName // .wmClass // "Unknown"), (.title // "")] + | @tsv + ' +) + +[ -n "$choices" ] || exit 0 + +# GNOME invokes custom keybindings before the shortcut is fully released. +# Give rofi a moment, then force the keyboard grab and focus takeover. +sleep 0.1 + +selection=$( + printf '%s\n' "$choices" | + cut -f2- | + rofi -dmenu -i -p "window" -format i -no-lazy-grab -steal-focus +) || exit 0 + +window_id=$( + printf '%s\n' "$choices" | + sed -n "$((selection + 1))p" | + cut -f1 +) + +[ -n "$window_id" ] || exit 1 + +activate_window "$window_id" diff --git a/bin/rofi b/bin/rofi new file mode 100755 index 0000000..cfa5a8f --- /dev/null +++ b/bin/rofi @@ -0,0 +1,30 @@ +#!/bin/sh + +# GNOME on Wayland does not support the layer-shell protocol that rofi's +# native Wayland backend requires, so fall back to Xwayland there. + +self_path=$(readlink -f "$0" 2>/dev/null || printf '%s\n' "$0") +rofi_bin="" + +for candidate in /usr/bin/rofi /bin/rofi; do + candidate_path=$(readlink -f "$candidate" 2>/dev/null || printf '%s\n' "$candidate") + if [ -x "$candidate" ] && [ "$candidate_path" != "$self_path" ]; then + rofi_bin="$candidate" + break + fi +done + +if [ -z "$rofi_bin" ]; then + printf '%s\n' "rofi: unable to find the system rofi binary" >&2 + exit 127 +fi + +if [ "${XDG_SESSION_TYPE:-}" = "wayland" ] && [ -n "${WAYLAND_DISPLAY:-}" ]; then + case "${XDG_CURRENT_DESKTOP:-}:${DESKTOP_SESSION:-}" in + *GNOME*:*|*:gnome|GNOME:*|gnome:*) + set -- -x11 "$@" + ;; + esac +fi + +exec "$rofi_bin" "$@" diff --git a/bin/setup-install-fonts b/bin/setup-install-fonts new file mode 100755 index 0000000..ea96873 --- /dev/null +++ b/bin/setup-install-fonts @@ -0,0 +1,26 @@ +#!/bin/bash + +set -e + +FONT_DIR="$HOME/.local/share/fonts" +TEMP_DIR=$(mktemp -d) + +mkdir -p "$FONT_DIR" + +pushd $TEMP_DIR +curl -s 'https://api.github.com/repos/be5invis/Iosevka/releases/latest' | \ + jq -r ".assets[] | .browser_download_url" | \ + grep PkgTTC-Iosevka | \ + xargs -n 1 curl -L -O --fail --silent --show-error + +for f in *.zip; do + unzip "$f" +done + +mv *.ttc "$FONT_DIR/" +fc-cache -f -v "$FONT_DIR" + +popd +rm -rf "$TEMP_DIR" + +echo "Iosevka fonts have been installed." |
