aboutsummaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
Diffstat (limited to 'bin')
-rwxr-xr-xbin/gnome-move-windows167
-rwxr-xr-xbin/gnome-set-config79
-rwxr-xr-xbin/gnome-window-switcher72
-rwxr-xr-xbin/rofi30
-rwxr-xr-xbin/setup-install-fonts26
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."
nihil fit ex nihilo