diff options
| author | pablu <me@pablu.de> | 2026-09-17 17:05:03 +0200 |
|---|---|---|
| committer | pablu <me@pablu.de> | 2026-09-17 17:05:03 +0200 |
| commit | f8312a59c775c740d9af173fafa65ab7e731e03e (patch) | |
| tree | a54a9274376f20bbb35f07bccfeab8aef542fa0e /dot_config/hypr | |
| parent | 0716da5eef605977b804f34f265979cf39be844b (diff) | |
update codex config to not contain local project dirs, update .tmux.conf only for gentoo, make tmux work with hyprland
Diffstat (limited to 'dot_config/hypr')
| -rw-r--r-- | dot_config/hypr/config/binds.lua | 54 | ||||
| -rw-r--r-- | dot_config/hypr/config/rules.lua | 10 | ||||
| -rw-r--r-- | dot_config/hypr/scripts/executable_herdr-smart-nav | 143 | ||||
| -rw-r--r-- | dot_config/hypr/scripts/executable_tmux-smart-nav | 105 |
4 files changed, 298 insertions, 14 deletions
diff --git a/dot_config/hypr/config/binds.lua b/dot_config/hypr/config/binds.lua index a5a9e6b..a72777f 100644 --- a/dot_config/hypr/config/binds.lua +++ b/dot_config/hypr/config/binds.lua @@ -52,6 +52,28 @@ local function ask_direct_nvim(win, direction) return response:find("success", 1, true) ~= nil end +local function ask_tmux(win, direction) + local pid = tonumber(win.pid) + + if not pid then + return false + end + + local helper = (os.getenv("HOME") or "") + .. "/.config/hypr/scripts/tmux-smart-nav" + + local handle = io.popen(string.format("%s %s %d", helper, direction, pid)) + + if not handle then + return false + end + + local result = handle:read("*a") + handle:close() + + return result:find("handled", 1, true) ~= nil +end + local function smart_nav(direction) return function() local win = hl.get_active_window() @@ -67,25 +89,29 @@ local function smart_nav(direction) local title = (win.title or ""):lower() - if title == "herdr" then + if ask_tmux(win, direction) then + return + end + + -- Herdr now uses changing window titles. The helper checks whether its + -- foreground client belongs to this terminal before attempting navigation. + local pid = tonumber(win.pid) + if pid then local helper = (os.getenv("HOME") or "") .. "/.config/hypr/scripts/herdr-smart-nav" + local handle = io.popen(string.format("%q %s %d", helper, direction, pid)) - local handle = io.popen(helper .. " " .. direction) + if handle then + local result = handle:read("*a"):match("^%s*(.-)%s*$") + handle:close() - if not handle then - focus_hyprland(direction) - return + if result == "handled" then + return + elseif result == "hyprland" then + focus_hyprland(direction) + return + end end - - local result = handle:read("*a") - handle:close() - - if result:find("hyprland", 1, true) then - focus_hyprland(direction) - end - - return end if is_direct_nvim(title) and ask_direct_nvim(win, direction) then diff --git a/dot_config/hypr/config/rules.lua b/dot_config/hypr/config/rules.lua index 0e17e4a..ca5fe88 100644 --- a/dot_config/hypr/config/rules.lua +++ b/dot_config/hypr/config/rules.lua @@ -73,3 +73,13 @@ hl.window_rule({ no_blur = true, no_shadow = true, }) + +hl.window_rule({ + name = "ghostty-fullscreen-opaque", + match = { + class = "com.mitchellh.ghostty", + fullscreen = true, + }, + force_rgbx = true, + no_blur = true, +}) diff --git a/dot_config/hypr/scripts/executable_herdr-smart-nav b/dot_config/hypr/scripts/executable_herdr-smart-nav new file mode 100644 index 0000000..36d27aa --- /dev/null +++ b/dot_config/hypr/scripts/executable_herdr-smart-nav @@ -0,0 +1,143 @@ +#!/usr/bin/env bash + +set -u + +direction="${1:-}" +terminal_pid="${2:-}" + +case "$direction" in + left|right|up|down) + ;; + *) + printf 'usage: %s <left|right|up|down> <terminal-pid>\n' "$0" >&2 + exit 2 + ;; +esac + +# Window titles can change with the workspace. Find a foreground Herdr +# process belonging to this terminal instead of relying on its title. +is_in_terminal() { + local pid="$1" + local parent + + while [[ "$pid" =~ ^[0-9]+$ && "$pid" -gt 1 ]]; do + [[ "$pid" == "$terminal_pid" ]] && return 0 + parent="$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d '[:space:]')" + [[ -n "$parent" && "$parent" != "$pid" ]] || return 1 + pid="$parent" + done + + return 1 +} + +herdr_found=false +if [[ "$terminal_pid" =~ ^[0-9]+$ ]]; then + while read -r pid process_group foreground_group; do + # The server has no foreground terminal; a suspended client is not active. + if [[ "$foreground_group" -gt 0 && "$process_group" == "$foreground_group" ]] && + is_in_terminal "$pid"; then + herdr_found=true + break + fi + done < <(ps -C herdr -o pid=,pgid=,tpgid= 2>/dev/null) +fi + +if [[ "$herdr_found" != true ]]; then + printf 'not-herdr\n' + exit +fi + +herdr_bin="${HERDR_BIN_PATH:-$HOME/.local/share/mise/installs/herdr/latest/herdr}" + +if [[ ! -x "$herdr_bin" ]]; then + printf 'hyprland\n' + exit +fi + +handled() { + printf 'handled\n' + exit +} + +fall_through() { + printf 'hyprland\n' + exit +} + +current="$("$herdr_bin" pane current --current 2>/dev/null)" || { + fall_through +} + +pane_id="$(jq -r '.result.pane.pane_id // empty' <<<"$current")" + +if [[ -z "$pane_id" ]]; then + fall_through +fi + +process_info="$( + "$herdr_bin" pane process-info --pane "$pane_id" 2>/dev/null || true +)" + +nvim_pid="$( + jq -r ' + [ + (.result.process_info.foreground_processes // [])[] + | select( + ((.name // "") | ascii_downcase) == "nvim" + or ((.name // "") | ascii_downcase) == "vim" + ) + | .pid + ][0] // empty + ' <<<"$process_info" 2>/dev/null +)" + +if [[ -n "$nvim_pid" ]]; then + safe_pane_id="${pane_id//[^a-zA-Z0-9_.-]/_}" + runtime_dir="${XDG_RUNTIME_DIR:-/tmp}" + + socket_path="$runtime_dir/nvim-hypr-nav-pane-$safe_pane_id.sock" + + response="$( + printf 'nav:%s\n' "$direction" | + socat -T 0.25 -,ignoreeof "UNIX-CONNECT:$socket_path" 2>/dev/null || + true + )" + + if [[ "$response" == *success* ]]; then + handled + fi +fi + +neighbor="$( + "$herdr_bin" pane neighbor \ + --pane "$pane_id" \ + --direction "$direction" \ + 2>/dev/null +)" || { + fall_through +} + +# A zoomed pane deliberately falls through to Hyprland. Do not expose or +# navigate to another Herdr pane. +if jq -e '.result.neighbor.layout.zoomed == true' \ + >/dev/null 2>&1 <<<"$neighbor"; then + fall_through +fi + +neighbor_id="$( + jq -r '.result.neighbor.neighbor_pane_id // empty' <<<"$neighbor" +)" + +# Herdr 0.7.5 returns the current pane itself when there is no neighbour. +if [[ -z "$neighbor_id" || "$neighbor_id" == "$pane_id" ]]; then + fall_through +fi + +if "$herdr_bin" pane focus \ + --pane "$pane_id" \ + --direction "$direction" \ + >/dev/null 2>&1; then + handled +else + fall_through +fi diff --git a/dot_config/hypr/scripts/executable_tmux-smart-nav b/dot_config/hypr/scripts/executable_tmux-smart-nav new file mode 100644 index 0000000..4275462 --- /dev/null +++ b/dot_config/hypr/scripts/executable_tmux-smart-nav @@ -0,0 +1,105 @@ +#!/usr/bin/env bash + +set -u + +direction="${1:-}" +terminal_pid="${2:-}" + +case "$direction" in + left) + tmux_key="C-h" + ;; + right) + tmux_key="C-l" + ;; + up) + tmux_key="C-k" + ;; + down) + tmux_key="C-j" + ;; + *) + printf 'usage: %s <left|right|up|down> <terminal-pid>\n' "$0" >&2 + exit 2 + ;; +esac + +if [[ ! "$terminal_pid" =~ ^[0-9]+$ ]] || ! command -v tmux >/dev/null 2>&1; then + printf 'hyprland\n' + exit +fi + +# A tmux client is a descendant of the terminal process that owns it. This +# avoids acting on a client in another Ghostty window. +is_descendant_of() { + local pid="$1" + local parent + + while [[ "$pid" =~ ^[0-9]+$ && "$pid" -gt 1 ]]; do + if [[ "$pid" == "$terminal_pid" ]]; then + return 0 + fi + + parent="$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d '[:space:]')" + [[ -n "$parent" && "$parent" != "$pid" ]] || return 1 + pid="$parent" + done + + return 1 +} + +client_tty="" +while read -r client_pid tty; do + if is_descendant_of "$client_pid"; then + client_tty="$tty" + break + fi +done < <(tmux list-clients -F '#{client_pid} #{client_tty}' 2>/dev/null) + +if [[ -z "$client_tty" ]]; then + printf 'hyprland\n' + exit +fi + +pane_id="$(tmux display-message -p -c "$client_tty" '#{pane_id}' 2>/dev/null)" +pane_command="$(tmux display-message -p -c "$client_tty" '#{pane_current_command}' 2>/dev/null)" + +# Ghostty has one title for the whole tmux window, so it cannot identify the +# focused pane's process. Ask Neovim through a pane-specific socket only when +# tmux says this exact pane is running it. +case "${pane_command,,}" in + nvim|vim) + safe_pane_id="${pane_id//[^a-zA-Z0-9_.-]/_}" + socket_path="${XDG_RUNTIME_DIR:-/tmp}/nvim-hypr-nav-pane-${safe_pane_id}.sock" + response="$( + printf 'nav:%s\n' "$direction" | + socat -T 0.25 -,ignoreeof "UNIX-CONNECT:$socket_path" 2>/dev/null || true + )" + + if [[ "$response" == *success* ]]; then + printf 'handled\n' + exit + fi + ;; +esac + +# Send the directional key through the matching client so tmux resolves it +# against that client's own active pane. Its existing C-h/j/k/l bindings also +# retain the configured Neovim handoff. +at_edge="$(tmux display-message -p -c "$client_tty" "#{pane_at_${direction}}" 2>/dev/null)" + +if [[ -z "$pane_id" || "$at_edge" == "1" ]]; then + printf 'hyprland\n' + exit +fi + +if tmux send-keys -c "$client_tty" -K "$tmux_key" >/dev/null 2>&1; then + selected_pane_id="$(tmux display-message -p -c "$client_tty" '#{pane_id}' 2>/dev/null)" + + if [[ "$selected_pane_id" != "$pane_id" ]]; then + printf 'handled\n' + exit + fi +fi + +printf 'hyprland\n' |
