From 43e03de85b98efc357ebe11194dbe5ef07651ff8 Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Sat, 28 Feb 2026 16:38:23 -0800 Subject: [PATCH 01/10] Add tmux statusbar auto-hide and mouse bindings Auto-hide statusbar when only 1 window is open, show when 2+. Add prefix+b toggle, middle-click to close tab, click interactions for status-left/right areas, and styled session indicator. --- dot_config/tmux/tmux.conf.tmpl | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/dot_config/tmux/tmux.conf.tmpl b/dot_config/tmux/tmux.conf.tmpl index ffb9326..0ac7696 100644 --- a/dot_config/tmux/tmux.conf.tmpl +++ b/dot_config/tmux/tmux.conf.tmpl @@ -147,8 +147,9 @@ bind n new-session # [omarchy override] Prefix+R is rename-session in omarchy; this rebinds to reload config # bind R source-file ~/.config/tmux/tmux.conf -# Mouse: double-click status bar for new window +# Mouse: double-click status bar for new window, middle-click tab to close bind -T root DoubleClick1Status new-window -c "#{pane_current_path}" +bind -T root MouseDown2Status kill-window -t= # ── Plugins ────────────────────────────────────────────────────────── set-environment -g TMUX_PLUGIN_MANAGER_PATH '~/.config/tmux/plugins' @@ -189,6 +190,8 @@ set -g @catppuccin_window_current_text " #T (#{window_panes})" set -g status-right-length 200 set -g status-left-length 100 set -g status-left "" +bind -T root MouseDown1StatusLeft new-window -c "#{pane_current_path}" +bind -T root MouseDown1StatusRight choose-tree -Zw set -g status 2 set -g status-format[1] "#[fg=#{@thm_surface_1},bg=default,fill=default]#(printf '·%.0s' $(seq 1 #{client_width}))" set -g status-right "#{E:@catppuccin_status_application}" @@ -196,6 +199,14 @@ set -agF status-right "#{E:@catppuccin_status_cpu}" set -ag status-right "#{E:@catppuccin_status_host}" set -ag status-right "#{E:@catppuccin_status_session}" +# ── Statusbar Auto-hide ────────────────────────────────────────────── +# Show statusbar only when >1 window; toggle manually with prefix+b +set-hook -g after-new-window 'if -F "#{==:#{session_windows},1}" "set status off" "set status 2"' +set-hook -g window-unlinked 'if -F "#{==:#{session_windows},1}" "set status off" "set status 2"' +set-hook -g client-session-changed 'if -F "#{==:#{session_windows},1}" "set status off" "set status 2"' +set-hook -g session-created 'set status off' +bind b if -F '#{==:#{status},off}' 'set status 2' 'set status off' + # ── Right-click Menu Fix ───────────────────────────────────────────── # Patch right-click menus: add -O flag so menus stay open after release run-shell 'tmux list-keys -T root | grep "MouseDown3.*display-menu" | sed "s/display-menu /display-menu -O /;s/-O -O /-O /" > /tmp/tmux-menu-fix.conf && tmux source-file /tmp/tmux-menu-fix.conf && rm -f /tmp/tmux-menu-fix.conf' @@ -207,3 +218,4 @@ run '~/.config/tmux/plugins/tpm/tpm' # otherwise the plugin's set -gF overwrites them). Solid bg breaks custom separators. set -g window-status-activity-style "bold" set -g window-status-bell-style "bold" +set -g status-left "#[fg=#313244,bg=default]#[fg=#cdd6f4,bg=#313244,bold]+#[fg=#313244,bg=default,nobold] " From fc12f518154b8367d909b14dd3415a9bae2d1cd6 Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Sat, 28 Feb 2026 18:04:35 -0800 Subject: [PATCH 02/10] Add distrobox container integration for tmux, zellij, and fish - Fish abbreviations: db, dbe, dbl, dbs, dbrm, dbc for distrobox commands - Fish function: container-fzf with fzf picker, supports tmux/zellij flags - tmux: prefix+e enters container in current pane, prefix+E in new window - tmux: container-aware splits (prefix+h/v inherit CONTAINER_ID) - tmux: status bar shows container name for active pane - zellij: session mode 'e' opens container picker in floating pane - zellij: container.kdl layout with host + picker tabs - Starship: container module shows name in prompt when inside distrobox --- dot_config/fish/conf.d/zz_04_abbr.fish | 9 ++ dot_config/fish/functions/container-fzf.fish | 122 ++++++++++++++++++ dot_config/starship.toml | 7 +- .../executable_container-pick-send.fish | 63 +++++++++ .../scripts/executable_container-split.sh | 39 ++++++ .../scripts/executable_container-status.sh | 20 +++ dot_config/tmux/tmux.conf.tmpl | 14 +- dot_config/zellij/config.kdl.tmpl | 7 + dot_config/zellij/layouts/container.kdl | 25 ++++ 9 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 dot_config/fish/functions/container-fzf.fish create mode 100644 dot_config/tmux/scripts/executable_container-pick-send.fish create mode 100755 dot_config/tmux/scripts/executable_container-split.sh create mode 100755 dot_config/tmux/scripts/executable_container-status.sh create mode 100644 dot_config/zellij/layouts/container.kdl diff --git a/dot_config/fish/conf.d/zz_04_abbr.fish b/dot_config/fish/conf.d/zz_04_abbr.fish index 070e148..18b0280 100644 --- a/dot_config/fish/conf.d/zz_04_abbr.fish +++ b/dot_config/fish/conf.d/zz_04_abbr.fish @@ -61,4 +61,13 @@ if status is-interactive # Broot abbr --add br 'broot' + + # --- Container management (distrobox) --- + + abbr --add db 'distrobox' + abbr --add dbe 'distrobox enter' + abbr --add dbl 'distrobox list' + abbr --add dbs 'distrobox stop' + abbr --add dbrm 'distrobox rm' + abbr --add dbc 'distrobox create' end diff --git a/dot_config/fish/functions/container-fzf.fish b/dot_config/fish/functions/container-fzf.fish new file mode 100644 index 0000000..6176112 --- /dev/null +++ b/dot_config/fish/functions/container-fzf.fish @@ -0,0 +1,122 @@ +# container-fzf: Interactive container picker using fzf +# Lists distrobox containers and enters the selected one. +# Used standalone or called by tmux/zellij integrations. +# +# Flags: +# --print-only Print the enter command instead of executing it +# --running-only Only show running containers +# --new-window Open selection in a new tmux window (tmux only) +# --split-h Open selection in a horizontal tmux split (tmux only) +# --split-v Open selection in a vertical tmux split (tmux only) +# --zellij-pane Open selection in a new zellij pane + +function container-fzf --description "Interactive distrobox container picker" + # Guard: requires distrobox and fzf + if not command -q distrobox + echo "container-fzf: distrobox not found" >&2 + return 1 + end + if not command -q fzf + echo "container-fzf: fzf not found" >&2 + return 1 + end + + # Parse flags + argparse 'print-only' 'running-only' 'new-window' 'split-h' 'split-v' 'zellij-pane' 'header=' -- $argv + or return 1 + + # Determine fzf header text + set -l fzf_header "Select container" + if set -q _flag_header + set fzf_header "$_flag_header" + else if set -q _flag_new_window + set fzf_header "Select container [new window]" + else if set -q _flag_zellij_pane + set fzf_header "Select container [new pane]" + end + + # Get container list, skip header line + # distrobox list format: ID | NAME | STATUS | IMAGE + set -l db_output (distrobox list --no-color 2>/dev/null | tail -n +2) + + if test (count $db_output) -eq 0 + echo "container-fzf: no distrobox containers found" >&2 + echo "Create one with: distrobox create --name --image " >&2 + return 1 + end + + # Filter to running only if requested + if set -q _flag_running_only + set db_output (printf '%s\n' $db_output | string match -r '.*Up.*') + if test (count $db_output) -eq 0 + echo "container-fzf: no running containers" >&2 + return 1 + end + end + + # Format for fzf: show NAME | STATUS | IMAGE (skip ID column) + # Extract just the name for selection + # Note: fzf uses $SHELL for preview, so we force sh -c for POSIX compat + set -l selected ( + printf '%s\n' $db_output | \ + fzf \ + --header "$fzf_header" \ + --preview 'sh -c '"'"'name=$(echo "$1" | awk -F"|" "{print \$2}" | xargs); echo "$1"; echo "---"; podman inspect --format "Image: {{.Config.Image}}\nCreated: {{.Created}}\nState: {{.State.Status}}" "$name" 2>/dev/null || echo "Container not running - will start on enter"'"'"' -- {}' \ + --prompt "container> " \ + --height 60% \ + --layout reverse \ + --border rounded \ + --ansi + ) + + if test -z "$selected" + return 0 # User cancelled + end + + # Extract container name (second column, pipe-delimited) + set -l container_name (echo $selected | awk -F'|' '{print $2}' | string trim) + + if test -z "$container_name" + echo "container-fzf: could not parse container name" >&2 + return 1 + end + + # --print-only: just output the command + if set -q _flag_print_only + echo "distrobox enter $container_name" + return 0 + end + + # tmux integrations + if set -q _flag_new_window + if test -n "$TMUX" + tmux new-window -n "$container_name" "distrobox enter $container_name" + return 0 + end + end + + if set -q _flag_split_h + if test -n "$TMUX" + tmux split-window -h "distrobox enter $container_name" + return 0 + end + end + + if set -q _flag_split_v + if test -n "$TMUX" + tmux split-window -v "distrobox enter $container_name" + return 0 + end + end + + # zellij integration + if set -q _flag_zellij_pane + if test -n "$ZELLIJ" + zellij run --name "$container_name" -- distrobox enter $container_name + return 0 + end + end + + # Default: enter the container directly in the current shell + distrobox enter $container_name +end diff --git a/dot_config/starship.toml b/dot_config/starship.toml index c48129b..707a274 100644 --- a/dot_config/starship.toml +++ b/dot_config/starship.toml @@ -11,7 +11,7 @@ add_newline = false # $character # """ format = """ -$cmd_duration 󰜥 $directory $git_branch +$cmd_duration$container 󰜥 $directory $git_branch $character""" # Replace the "❯" symbol in the prompt with "➜" @@ -95,6 +95,11 @@ format = '[](bold fg:#a6e3a1)[󰉋 $path]($style)[](bold fg:#a6e3a1)' "Videos" = "  " "GitHub" = " 󰊤 " +[container] +symbol = "󰏖" +style = "bold bg:#cba6f7 fg:#11111b" +format = ' [](bold fg:#cba6f7)[$symbol $name]($style)[](bold fg:#cba6f7)' + [cmd_duration] min_time = 0 format = '[](bold fg:#f9e2af)[󰪢 $duration](bold bg:#f9e2af fg:#11111b)[](bold fg:#f9e2af)' diff --git a/dot_config/tmux/scripts/executable_container-pick-send.fish b/dot_config/tmux/scripts/executable_container-pick-send.fish new file mode 100644 index 0000000..510daf3 --- /dev/null +++ b/dot_config/tmux/scripts/executable_container-pick-send.fish @@ -0,0 +1,63 @@ +#!/usr/bin/env fish +# container-pick-send.fish — Pick a container via fzf, then send the enter +# command to the originating tmux pane (not the popup). +# +# Usage: container-pick-send.fish + +set pane_id $argv[1] +if test -z "$pane_id" + echo "Usage: container-pick-send.fish " >&2 + read -P "Press enter to close..." + exit 1 +end + +# Guard: requires distrobox and fzf +if not command -q distrobox + echo "container-pick-send: distrobox not found" >&2 + read -P "Press enter to close..." + exit 1 +end +if not command -q fzf + echo "container-pick-send: fzf not found" >&2 + read -P "Press enter to close..." + exit 1 +end + +# Get container list, skip header line +set -l db_output (distrobox list --no-color 2>/dev/null | tail -n +2) + +if test (count $db_output) -eq 0 + echo "No distrobox containers found" + echo "Create one with: distrobox create --name --image " + read -P "Press enter to close..." + exit 1 +end + +# Run fzf directly (not inside command substitution from another function) +# This ensures fzf gets the popup's TTY +set -l selected ( + printf '%s\n' $db_output | \ + fzf \ + --header "Select container [current pane]" \ + --preview 'sh -c '"'"'name=$(echo "$1" | awk -F"|" "{print \$2}" | xargs); echo "$1"; echo "---"; podman inspect --format "Image: {{.Config.Image}}\nCreated: {{.Created}}\nState: {{.State.Status}}" "$name" 2>/dev/null || echo "Container not running - will start on enter"'"'"' -- {}' \ + --prompt "container> " \ + --layout reverse \ + --border rounded \ + --ansi +) + +if test -z "$selected" + exit 0 +end + +# Extract container name (second column, pipe-delimited) +set -l container_name (echo $selected | awk -F'|' '{print $2}' | string trim) + +if test -z "$container_name" + echo "Could not parse container name" >&2 + read -P "Press enter to close..." + exit 1 +end + +# Send the command as keystrokes to the original pane +tmux send-keys -t "$pane_id" "distrobox enter $container_name" Enter diff --git a/dot_config/tmux/scripts/executable_container-split.sh b/dot_config/tmux/scripts/executable_container-split.sh new file mode 100755 index 0000000..9d2e9d8 --- /dev/null +++ b/dot_config/tmux/scripts/executable_container-split.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# container-split.sh — Container-aware tmux split +# If the current pane is inside a distrobox container, the new split +# automatically enters the same container. Otherwise, opens a normal shell. +# +# Usage: container-split.sh [-h|-v] +# -h horizontal split (side by side) +# -v vertical split (top/bottom) + +set -euo pipefail + +SPLIT_DIR="${1:--h}" + +# Get the active pane's PID, then walk its child processes to find CONTAINER_ID +PANE_PID=$(tmux display-message -p '#{pane_pid}') +CONTAINER_ID="" + +# Check the pane's environment for CONTAINER_ID +# Walk the process tree to find a shell that has CONTAINER_ID set +if [ -n "$PANE_PID" ]; then + # Try to read CONTAINER_ID from any child process of the pane + for pid in $(pgrep -P "$PANE_PID" 2>/dev/null) $PANE_PID; do + if [ -r "/proc/$pid/environ" ]; then + cid=$(tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | grep '^CONTAINER_ID=' | head -1 | cut -d= -f2-) + if [ -n "$cid" ]; then + CONTAINER_ID="$cid" + break + fi + fi + done +fi + +if [ -n "$CONTAINER_ID" ]; then + # Inside a container — new split enters the same container + tmux split-window "$SPLIT_DIR" -c "#{pane_current_path}" "distrobox enter $CONTAINER_ID" +else + # On host — normal split + tmux split-window "$SPLIT_DIR" -c "#{pane_current_path}" +fi diff --git a/dot_config/tmux/scripts/executable_container-status.sh b/dot_config/tmux/scripts/executable_container-status.sh new file mode 100755 index 0000000..e0a6c01 --- /dev/null +++ b/dot_config/tmux/scripts/executable_container-status.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# container-status.sh — Show the container name for the active tmux pane +# Returns the CONTAINER_ID if the active pane is inside a distrobox/toolbox, +# or empty string if on the host. Used in tmux status-right. + +set -euo pipefail + +PANE_PID=$(tmux display-message -p '#{pane_pid}' 2>/dev/null) +[ -z "$PANE_PID" ] && exit 0 + +# Walk the pane's process tree looking for CONTAINER_ID +for pid in $(pgrep -P "$PANE_PID" 2>/dev/null) $PANE_PID; do + if [ -r "/proc/$pid/environ" ]; then + cid=$(tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | grep '^CONTAINER_ID=' | head -1 | cut -d= -f2-) + if [ -n "$cid" ]; then + echo "󰏖 $cid" + exit 0 + fi + fi +done diff --git a/dot_config/tmux/tmux.conf.tmpl b/dot_config/tmux/tmux.conf.tmpl index 0ac7696..d58472d 100644 --- a/dot_config/tmux/tmux.conf.tmpl +++ b/dot_config/tmux/tmux.conf.tmpl @@ -147,6 +147,17 @@ bind n new-session # [omarchy override] Prefix+R is rename-session in omarchy; this rebinds to reload config # bind R source-file ~/.config/tmux/tmux.conf +# ── Container Integration ───────────────────────────────────────────── +# prefix+e: fzf picker popup — enter selected container in current pane +# prefix+E: fzf picker popup — open selected container in a new window +bind e run-shell 'tmux display-popup -E -w 80% -h 60% "~/.config/tmux/scripts/container-pick-send.fish #{pane_id}"' +bind E display-popup -E -w 80% -h 60% "fish -c 'container-fzf --new-window'" + +# Container-aware splits: if current pane is inside a container, new splits +# auto-enter the same container. Falls back to normal split on host. +bind h run-shell "~/.config/tmux/scripts/container-split.sh -h" +bind v run-shell "~/.config/tmux/scripts/container-split.sh -v" + # Mouse: double-click status bar for new window, middle-click tab to close bind -T root DoubleClick1Status new-window -c "#{pane_current_path}" bind -T root MouseDown2Status kill-window -t= @@ -194,7 +205,8 @@ bind -T root MouseDown1StatusLeft new-window -c "#{pane_current_path}" bind -T root MouseDown1StatusRight choose-tree -Zw set -g status 2 set -g status-format[1] "#[fg=#{@thm_surface_1},bg=default,fill=default]#(printf '·%.0s' $(seq 1 #{client_width}))" -set -g status-right "#{E:@catppuccin_status_application}" +set -g status-right "#[fg=#{@thm_mauve}]#(~/.config/tmux/scripts/container-status.sh) " +set -agF status-right "#{E:@catppuccin_status_application}" set -agF status-right "#{E:@catppuccin_status_cpu}" set -ag status-right "#{E:@catppuccin_status_host}" set -ag status-right "#{E:@catppuccin_status_session}" diff --git a/dot_config/zellij/config.kdl.tmpl b/dot_config/zellij/config.kdl.tmpl index d70b613..e7cd7cf 100644 --- a/dot_config/zellij/config.kdl.tmpl +++ b/dot_config/zellij/config.kdl.tmpl @@ -96,6 +96,13 @@ keybinds clear-defaults=true { bind "w" { SearchToggleOption "Wrap"; } } session { + // Container picker: fzf-based distrobox selector in a floating pane + bind "e" { + Run "fish" "-c" "container-fzf --zellij-pane" { + direction "Down" + } + SwitchToMode "normal" + } bind "a" { LaunchOrFocusPlugin "zellij:about" { floating true diff --git a/dot_config/zellij/layouts/container.kdl b/dot_config/zellij/layouts/container.kdl new file mode 100644 index 0000000..9ca2776 --- /dev/null +++ b/dot_config/zellij/layouts/container.kdl @@ -0,0 +1,25 @@ +// Container development layout for zellij +// Usage: zellij --layout container +// +// Opens a host tab and a container picker tab. +// To create a layout with a specific container, use: +// zellij --layout container --session my-project + +layout { + default_tab_template { + pane size=1 borderless=true { + plugin location="compact-bar" + } + children + } + + tab name="host" focus=true { + pane name="shell" + } + + tab name="container" { + pane name="picker" command="fish" { + args "-c" "container-fzf" + } + } +} From 7a0ee0424a71da295d61a8a98527d060c2ead600 Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Sat, 28 Feb 2026 18:40:11 -0800 Subject: [PATCH 03/10] Fix zellij container picker: multi-action fzf with close_on_exit - Single keybind (Ctrl+o, e) opens floating picker with three actions: enter=current pane, alt-s=tiled split, alt-t=new tab - Toggle floating off before creating panes to fix focus issues - Use alt-s/alt-t instead of ctrl-s/ctrl-t to avoid zellij interception - Add close_on_exit to picker pane for clean auto-dismiss --- dot_config/fish/functions/container-fzf.fish | 85 ++++++++++++++------ dot_config/zellij/config.kdl.tmpl | 7 +- 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/dot_config/fish/functions/container-fzf.fish b/dot_config/fish/functions/container-fzf.fish index 6176112..9fc9566 100644 --- a/dot_config/fish/functions/container-fzf.fish +++ b/dot_config/fish/functions/container-fzf.fish @@ -8,7 +8,7 @@ # --new-window Open selection in a new tmux window (tmux only) # --split-h Open selection in a horizontal tmux split (tmux only) # --split-v Open selection in a vertical tmux split (tmux only) -# --zellij-pane Open selection in a new zellij pane +# --zellij Zellij mode: multi-action picker (enter/ctrl-s/ctrl-t) function container-fzf --description "Interactive distrobox container picker" # Guard: requires distrobox and fzf @@ -22,7 +22,7 @@ function container-fzf --description "Interactive distrobox container picker" end # Parse flags - argparse 'print-only' 'running-only' 'new-window' 'split-h' 'split-v' 'zellij-pane' 'header=' -- $argv + argparse 'print-only' 'running-only' 'new-window' 'split-h' 'split-v' 'zellij' 'header=' -- $argv or return 1 # Determine fzf header text @@ -31,8 +31,8 @@ function container-fzf --description "Interactive distrobox container picker" set fzf_header "$_flag_header" else if set -q _flag_new_window set fzf_header "Select container [new window]" - else if set -q _flag_zellij_pane - set fzf_header "Select container [new pane]" + else if set -q _flag_zellij + set fzf_header "enter: current pane | alt-s: split | alt-t: new tab" end # Get container list, skip header line @@ -54,25 +54,42 @@ function container-fzf --description "Interactive distrobox container picker" end end - # Format for fzf: show NAME | STATUS | IMAGE (skip ID column) - # Extract just the name for selection - # Note: fzf uses $SHELL for preview, so we force sh -c for POSIX compat - set -l selected ( - printf '%s\n' $db_output | \ - fzf \ - --header "$fzf_header" \ - --preview 'sh -c '"'"'name=$(echo "$1" | awk -F"|" "{print \$2}" | xargs); echo "$1"; echo "---"; podman inspect --format "Image: {{.Config.Image}}\nCreated: {{.Created}}\nState: {{.State.Status}}" "$name" 2>/dev/null || echo "Container not running - will start on enter"'"'"' -- {}' \ - --prompt "container> " \ - --height 60% \ - --layout reverse \ - --border rounded \ - --ansi - ) + # Build fzf options + set -l fzf_opts \ + --header "$fzf_header" \ + --preview 'sh -c '"'"'name=$(echo "$1" | awk -F"|" "{print \$2}" | xargs); echo "$1"; echo "---"; podman inspect --format "Image: {{.Config.Image}}\nCreated: {{.Created}}\nState: {{.State.Status}}" "$name" 2>/dev/null || echo "Container not running - will start on enter"'"'"' -- {}' \ + --prompt "container> " \ + --layout reverse \ + --border rounded \ + --ansi + + # In zellij mode, use --expect to capture which key was pressed + if set -q _flag_zellij + set -a fzf_opts --expect "alt-s,alt-t" + end - if test -z "$selected" + # Run fzf + set -l fzf_output (printf '%s\n' $db_output | fzf $fzf_opts) + + if test (count $fzf_output) -eq 0 return 0 # User cancelled end + # In zellij mode, first line is the key pressed, second is the selection + # In normal mode, the only line is the selection + set -l key "" + set -l selected "" + if set -q _flag_zellij + set key $fzf_output[1] + set selected $fzf_output[2] + else + set selected $fzf_output[1] + end + + if test -z "$selected" + return 0 + end + # Extract container name (second column, pipe-delimited) set -l container_name (echo $selected | awk -F'|' '{print $2}' | string trim) @@ -109,12 +126,32 @@ function container-fzf --description "Interactive distrobox container picker" end end - # zellij integration - if set -q _flag_zellij_pane - if test -n "$ZELLIJ" - zellij run --name "$container_name" -- distrobox enter $container_name - return 0 + # zellij multi-action mode + # Picker pane has close_on_exit=true, so it auto-closes when script exits. + # For "enter", exec replaces the picker process so the pane stays as container. + if set -q _flag_zellij + switch "$key" + case "alt-s" + # Hide floating picker, create tiled pane (gets focus), write command + zellij action toggle-floating-panes + zellij action new-pane -d down --name "$container_name" + sleep 0.3 + zellij action write-chars "distrobox enter $container_name" + zellij action write 10 + case "alt-t" + # New tab; write distrobox command into it; picker auto-closes on exit + zellij action new-tab --name "$container_name" + sleep 0.3 + zellij action write-chars "distrobox enter $container_name" + zellij action write 10 + case "" + # Enter key: hide picker, write command to the original pane + zellij action toggle-floating-panes + sleep 0.3 + zellij action write-chars "distrobox enter $container_name" + zellij action write 10 end + return 0 end # Default: enter the container directly in the current shell diff --git a/dot_config/zellij/config.kdl.tmpl b/dot_config/zellij/config.kdl.tmpl index e7cd7cf..a1c3228 100644 --- a/dot_config/zellij/config.kdl.tmpl +++ b/dot_config/zellij/config.kdl.tmpl @@ -96,10 +96,11 @@ keybinds clear-defaults=true { bind "w" { SearchToggleOption "Wrap"; } } session { - // Container picker: fzf-based distrobox selector in a floating pane + // Container picker: enter=current pane, alt-s=split, alt-t=new tab bind "e" { - Run "fish" "-c" "container-fzf --zellij-pane" { - direction "Down" + Run "fish" "-c" "container-fzf --zellij" { + floating true + close_on_exit true } SwitchToMode "normal" } From 4a676eb185f77d1fd476f95f2c6d071181553c9d Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Sat, 28 Feb 2026 19:04:22 -0800 Subject: [PATCH 04/10] Refine zellij container picker actions - enter: writes command to current pane - alt-s: new tiled split with distrobox as pane command (direct) - alt-t: new tab via temp KDL layout with compact-bar/status-bar chrome - Remove broken --in-place and break-pane approaches --- dot_config/fish/functions/container-fzf.fish | 33 +++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/dot_config/fish/functions/container-fzf.fish b/dot_config/fish/functions/container-fzf.fish index 9fc9566..9d5b1ba 100644 --- a/dot_config/fish/functions/container-fzf.fish +++ b/dot_config/fish/functions/container-fzf.fish @@ -128,24 +128,33 @@ function container-fzf --description "Interactive distrobox container picker" # zellij multi-action mode # Picker pane has close_on_exit=true, so it auto-closes when script exits. - # For "enter", exec replaces the picker process so the pane stays as container. + # Uses --in-place or -- COMMAND to launch distrobox as the pane's process + # (no leftover shell command text, clean pane title). if set -q _flag_zellij switch "$key" case "alt-s" - # Hide floating picker, create tiled pane (gets focus), write command + # Tiled split running distrobox directly zellij action toggle-floating-panes - zellij action new-pane -d down --name "$container_name" - sleep 0.3 - zellij action write-chars "distrobox enter $container_name" - zellij action write 10 + zellij action new-pane -d down --name "$container_name" -- distrobox enter $container_name case "alt-t" - # New tab; write distrobox command into it; picker auto-closes on exit - zellij action new-tab --name "$container_name" - sleep 0.3 - zellij action write-chars "distrobox enter $container_name" - zellij action write 10 + # New tab: generate a temp layout with the distrobox command, open as tab + set -l layout_file (mktemp /tmp/zellij-container-XXXXXX.kdl) + printf 'layout { + pane size=1 borderless=true { + plugin location="compact-bar" + } + pane command="distrobox" { + args "enter" "%s" + name "%s" + } + pane size=1 borderless=true { + plugin location="status-bar" + } +}\n' $container_name $container_name > $layout_file + zellij action new-tab --layout "$layout_file" --name "$container_name" + rm -f "$layout_file" case "" - # Enter key: hide picker, write command to the original pane + # Current pane: send distrobox command to the original pane zellij action toggle-floating-panes sleep 0.3 zellij action write-chars "distrobox enter $container_name" From e23f8fe191a82af144c0bd5e9c6ec9dbc2fac743 Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Sat, 28 Feb 2026 19:08:55 -0800 Subject: [PATCH 05/10] Replace current-pane text injection with floating pane in zellij picker All three zellij picker actions now launch distrobox as a direct pane command: enter=floating, alt-s=tiled split, alt-t=new tab via temp layout --- dot_config/fish/functions/container-fzf.fish | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/dot_config/fish/functions/container-fzf.fish b/dot_config/fish/functions/container-fzf.fish index 9d5b1ba..7e96de9 100644 --- a/dot_config/fish/functions/container-fzf.fish +++ b/dot_config/fish/functions/container-fzf.fish @@ -32,7 +32,7 @@ function container-fzf --description "Interactive distrobox container picker" else if set -q _flag_new_window set fzf_header "Select container [new window]" else if set -q _flag_zellij - set fzf_header "enter: current pane | alt-s: split | alt-t: new tab" + set fzf_header "enter: floating | alt-s: split | alt-t: new tab" end # Get container list, skip header line @@ -128,8 +128,7 @@ function container-fzf --description "Interactive distrobox container picker" # zellij multi-action mode # Picker pane has close_on_exit=true, so it auto-closes when script exits. - # Uses --in-place or -- COMMAND to launch distrobox as the pane's process - # (no leftover shell command text, clean pane title). + # All actions launch distrobox as the pane's command directly (no shell wrapper). if set -q _flag_zellij switch "$key" case "alt-s" @@ -154,11 +153,8 @@ function container-fzf --description "Interactive distrobox container picker" zellij action new-tab --layout "$layout_file" --name "$container_name" rm -f "$layout_file" case "" - # Current pane: send distrobox command to the original pane - zellij action toggle-floating-panes - sleep 0.3 - zellij action write-chars "distrobox enter $container_name" - zellij action write 10 + # Floating pane running distrobox directly + zellij action new-pane --floating --name "$container_name" -- distrobox enter $container_name end return 0 end From 402e2ad4824b22ede044d5d76c3ad329bc30529f Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Sat, 28 Feb 2026 21:17:54 -0800 Subject: [PATCH 06/10] Unify tmux and zellij container picker into single multi-action UI - Add --tmux flag: enter=new window, alt-h=hsplit, alt-v=vsplit - Remove old single-action flags (--new-window, --split-h, --split-v) - Simplify tmux popup script to one-line wrapper calling container-fzf --tmux - Single prefix+e keybind replaces prefix+e and prefix+E - Both multiplexers now use the same fzf --expect pattern --- dot_config/fish/functions/container-fzf.fish | 64 +++++++++--------- .../executable_container-pick-send.fish | 65 ++----------------- dot_config/tmux/tmux.conf.tmpl | 6 +- 3 files changed, 35 insertions(+), 100 deletions(-) diff --git a/dot_config/fish/functions/container-fzf.fish b/dot_config/fish/functions/container-fzf.fish index 7e96de9..7e9adfa 100644 --- a/dot_config/fish/functions/container-fzf.fish +++ b/dot_config/fish/functions/container-fzf.fish @@ -3,12 +3,11 @@ # Used standalone or called by tmux/zellij integrations. # # Flags: +# --tmux tmux mode: enter=new window, alt-h=hsplit, alt-v=vsplit +# --zellij zellij mode: enter=floating, alt-s=split, alt-t=new tab # --print-only Print the enter command instead of executing it # --running-only Only show running containers -# --new-window Open selection in a new tmux window (tmux only) -# --split-h Open selection in a horizontal tmux split (tmux only) -# --split-v Open selection in a vertical tmux split (tmux only) -# --zellij Zellij mode: multi-action picker (enter/ctrl-s/ctrl-t) +# --header=TEXT Override the fzf header text function container-fzf --description "Interactive distrobox container picker" # Guard: requires distrobox and fzf @@ -22,15 +21,15 @@ function container-fzf --description "Interactive distrobox container picker" end # Parse flags - argparse 'print-only' 'running-only' 'new-window' 'split-h' 'split-v' 'zellij' 'header=' -- $argv + argparse 'print-only' 'running-only' 'tmux' 'zellij' 'header=' -- $argv or return 1 # Determine fzf header text set -l fzf_header "Select container" if set -q _flag_header set fzf_header "$_flag_header" - else if set -q _flag_new_window - set fzf_header "Select container [new window]" + else if set -q _flag_tmux + set fzf_header "enter: new window | alt-h: hsplit | alt-v: vsplit" else if set -q _flag_zellij set fzf_header "enter: floating | alt-s: split | alt-t: new tab" end @@ -63,8 +62,10 @@ function container-fzf --description "Interactive distrobox container picker" --border rounded \ --ansi - # In zellij mode, use --expect to capture which key was pressed - if set -q _flag_zellij + # Multi-action modes use --expect to capture which key was pressed + if set -q _flag_tmux + set -a fzf_opts --expect "alt-h,alt-v" + else if set -q _flag_zellij set -a fzf_opts --expect "alt-s,alt-t" end @@ -75,11 +76,11 @@ function container-fzf --description "Interactive distrobox container picker" return 0 # User cancelled end - # In zellij mode, first line is the key pressed, second is the selection - # In normal mode, the only line is the selection + # With --expect, first line is the key pressed, second is the selection + # Without --expect, the only line is the selection set -l key "" set -l selected "" - if set -q _flag_zellij + if set -q _flag_tmux; or set -q _flag_zellij set key $fzf_output[1] set selected $fzf_output[2] else @@ -104,31 +105,24 @@ function container-fzf --description "Interactive distrobox container picker" return 0 end - # tmux integrations - if set -q _flag_new_window - if test -n "$TMUX" - tmux new-window -n "$container_name" "distrobox enter $container_name" - return 0 - end - end - - if set -q _flag_split_h - if test -n "$TMUX" - tmux split-window -h "distrobox enter $container_name" - return 0 - end - end - - if set -q _flag_split_v - if test -n "$TMUX" - tmux split-window -v "distrobox enter $container_name" - return 0 + # ── tmux multi-action mode ─────────────────────────────────────── + # All actions run distrobox as the pane/window command directly. + # The popup closes automatically when this script exits. + if set -q _flag_tmux + switch "$key" + case "alt-h" + tmux split-window -h -c "#{pane_current_path}" "distrobox enter $container_name" + case "alt-v" + tmux split-window -v -c "#{pane_current_path}" "distrobox enter $container_name" + case "" + tmux new-window -n "$container_name" "distrobox enter $container_name" end + return 0 end - # zellij multi-action mode + # ── zellij multi-action mode ───────────────────────────────────── # Picker pane has close_on_exit=true, so it auto-closes when script exits. - # All actions launch distrobox as the pane's command directly (no shell wrapper). + # All actions launch distrobox as the pane's command directly. if set -q _flag_zellij switch "$key" case "alt-s" @@ -136,7 +130,7 @@ function container-fzf --description "Interactive distrobox container picker" zellij action toggle-floating-panes zellij action new-pane -d down --name "$container_name" -- distrobox enter $container_name case "alt-t" - # New tab: generate a temp layout with the distrobox command, open as tab + # New tab via temp layout with UI chrome set -l layout_file (mktemp /tmp/zellij-container-XXXXXX.kdl) printf 'layout { pane size=1 borderless=true { @@ -159,6 +153,6 @@ function container-fzf --description "Interactive distrobox container picker" return 0 end - # Default: enter the container directly in the current shell + # Default (no multiplexer flag): enter the container in the current shell distrobox enter $container_name end diff --git a/dot_config/tmux/scripts/executable_container-pick-send.fish b/dot_config/tmux/scripts/executable_container-pick-send.fish index 510daf3..e5262f7 100644 --- a/dot_config/tmux/scripts/executable_container-pick-send.fish +++ b/dot_config/tmux/scripts/executable_container-pick-send.fish @@ -1,63 +1,6 @@ #!/usr/bin/env fish -# container-pick-send.fish — Pick a container via fzf, then send the enter -# command to the originating tmux pane (not the popup). -# -# Usage: container-pick-send.fish +# container-pick-send.fish — tmux popup wrapper for container-fzf +# Runs the multi-action container picker inside a tmux popup. +# The popup provides the TTY that fzf needs. -set pane_id $argv[1] -if test -z "$pane_id" - echo "Usage: container-pick-send.fish " >&2 - read -P "Press enter to close..." - exit 1 -end - -# Guard: requires distrobox and fzf -if not command -q distrobox - echo "container-pick-send: distrobox not found" >&2 - read -P "Press enter to close..." - exit 1 -end -if not command -q fzf - echo "container-pick-send: fzf not found" >&2 - read -P "Press enter to close..." - exit 1 -end - -# Get container list, skip header line -set -l db_output (distrobox list --no-color 2>/dev/null | tail -n +2) - -if test (count $db_output) -eq 0 - echo "No distrobox containers found" - echo "Create one with: distrobox create --name --image " - read -P "Press enter to close..." - exit 1 -end - -# Run fzf directly (not inside command substitution from another function) -# This ensures fzf gets the popup's TTY -set -l selected ( - printf '%s\n' $db_output | \ - fzf \ - --header "Select container [current pane]" \ - --preview 'sh -c '"'"'name=$(echo "$1" | awk -F"|" "{print \$2}" | xargs); echo "$1"; echo "---"; podman inspect --format "Image: {{.Config.Image}}\nCreated: {{.Created}}\nState: {{.State.Status}}" "$name" 2>/dev/null || echo "Container not running - will start on enter"'"'"' -- {}' \ - --prompt "container> " \ - --layout reverse \ - --border rounded \ - --ansi -) - -if test -z "$selected" - exit 0 -end - -# Extract container name (second column, pipe-delimited) -set -l container_name (echo $selected | awk -F'|' '{print $2}' | string trim) - -if test -z "$container_name" - echo "Could not parse container name" >&2 - read -P "Press enter to close..." - exit 1 -end - -# Send the command as keystrokes to the original pane -tmux send-keys -t "$pane_id" "distrobox enter $container_name" Enter +container-fzf --tmux diff --git a/dot_config/tmux/tmux.conf.tmpl b/dot_config/tmux/tmux.conf.tmpl index d58472d..cfeb589 100644 --- a/dot_config/tmux/tmux.conf.tmpl +++ b/dot_config/tmux/tmux.conf.tmpl @@ -148,10 +148,8 @@ bind n new-session # bind R source-file ~/.config/tmux/tmux.conf # ── Container Integration ───────────────────────────────────────────── -# prefix+e: fzf picker popup — enter selected container in current pane -# prefix+E: fzf picker popup — open selected container in a new window -bind e run-shell 'tmux display-popup -E -w 80% -h 60% "~/.config/tmux/scripts/container-pick-send.fish #{pane_id}"' -bind E display-popup -E -w 80% -h 60% "fish -c 'container-fzf --new-window'" +# prefix+e: fzf container picker (enter=new window, alt-h=hsplit, alt-v=vsplit) +bind e display-popup -E -w 80% -h 60% "~/.config/tmux/scripts/container-pick-send.fish" # Container-aware splits: if current pane is inside a container, new splits # auto-enter the same container. Falls back to normal split on host. From 4e55120ff3ef641706c8a37641017d6ce605005e Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Sat, 28 Feb 2026 21:33:55 -0800 Subject: [PATCH 07/10] Convert container-fzf from fish function to POSIX bash script - Move container-fzf to ~/.local/bin/ as a bash script (works from any shell) - Remove fish-only container-fzf.fish function - Convert container-pick-send from .fish to .sh - Update tmux/zellij configs to call container-fzf directly (no fish wrapper) - Fix fzf preview to use explicit sh -c to avoid fish /bin/fish interference --- dot_config/fish/functions/container-fzf.fish | 158 ---------------- .../executable_container-pick-send.fish | 6 - .../scripts/executable_container-pick-send.sh | 6 + dot_config/tmux/tmux.conf.tmpl | 2 +- dot_config/zellij/config.kdl.tmpl | 4 +- dot_local/bin/executable_container-fzf | 176 ++++++++++++++++++ 6 files changed, 185 insertions(+), 167 deletions(-) delete mode 100644 dot_config/fish/functions/container-fzf.fish delete mode 100644 dot_config/tmux/scripts/executable_container-pick-send.fish create mode 100644 dot_config/tmux/scripts/executable_container-pick-send.sh create mode 100644 dot_local/bin/executable_container-fzf diff --git a/dot_config/fish/functions/container-fzf.fish b/dot_config/fish/functions/container-fzf.fish deleted file mode 100644 index 7e9adfa..0000000 --- a/dot_config/fish/functions/container-fzf.fish +++ /dev/null @@ -1,158 +0,0 @@ -# container-fzf: Interactive container picker using fzf -# Lists distrobox containers and enters the selected one. -# Used standalone or called by tmux/zellij integrations. -# -# Flags: -# --tmux tmux mode: enter=new window, alt-h=hsplit, alt-v=vsplit -# --zellij zellij mode: enter=floating, alt-s=split, alt-t=new tab -# --print-only Print the enter command instead of executing it -# --running-only Only show running containers -# --header=TEXT Override the fzf header text - -function container-fzf --description "Interactive distrobox container picker" - # Guard: requires distrobox and fzf - if not command -q distrobox - echo "container-fzf: distrobox not found" >&2 - return 1 - end - if not command -q fzf - echo "container-fzf: fzf not found" >&2 - return 1 - end - - # Parse flags - argparse 'print-only' 'running-only' 'tmux' 'zellij' 'header=' -- $argv - or return 1 - - # Determine fzf header text - set -l fzf_header "Select container" - if set -q _flag_header - set fzf_header "$_flag_header" - else if set -q _flag_tmux - set fzf_header "enter: new window | alt-h: hsplit | alt-v: vsplit" - else if set -q _flag_zellij - set fzf_header "enter: floating | alt-s: split | alt-t: new tab" - end - - # Get container list, skip header line - # distrobox list format: ID | NAME | STATUS | IMAGE - set -l db_output (distrobox list --no-color 2>/dev/null | tail -n +2) - - if test (count $db_output) -eq 0 - echo "container-fzf: no distrobox containers found" >&2 - echo "Create one with: distrobox create --name --image " >&2 - return 1 - end - - # Filter to running only if requested - if set -q _flag_running_only - set db_output (printf '%s\n' $db_output | string match -r '.*Up.*') - if test (count $db_output) -eq 0 - echo "container-fzf: no running containers" >&2 - return 1 - end - end - - # Build fzf options - set -l fzf_opts \ - --header "$fzf_header" \ - --preview 'sh -c '"'"'name=$(echo "$1" | awk -F"|" "{print \$2}" | xargs); echo "$1"; echo "---"; podman inspect --format "Image: {{.Config.Image}}\nCreated: {{.Created}}\nState: {{.State.Status}}" "$name" 2>/dev/null || echo "Container not running - will start on enter"'"'"' -- {}' \ - --prompt "container> " \ - --layout reverse \ - --border rounded \ - --ansi - - # Multi-action modes use --expect to capture which key was pressed - if set -q _flag_tmux - set -a fzf_opts --expect "alt-h,alt-v" - else if set -q _flag_zellij - set -a fzf_opts --expect "alt-s,alt-t" - end - - # Run fzf - set -l fzf_output (printf '%s\n' $db_output | fzf $fzf_opts) - - if test (count $fzf_output) -eq 0 - return 0 # User cancelled - end - - # With --expect, first line is the key pressed, second is the selection - # Without --expect, the only line is the selection - set -l key "" - set -l selected "" - if set -q _flag_tmux; or set -q _flag_zellij - set key $fzf_output[1] - set selected $fzf_output[2] - else - set selected $fzf_output[1] - end - - if test -z "$selected" - return 0 - end - - # Extract container name (second column, pipe-delimited) - set -l container_name (echo $selected | awk -F'|' '{print $2}' | string trim) - - if test -z "$container_name" - echo "container-fzf: could not parse container name" >&2 - return 1 - end - - # --print-only: just output the command - if set -q _flag_print_only - echo "distrobox enter $container_name" - return 0 - end - - # ── tmux multi-action mode ─────────────────────────────────────── - # All actions run distrobox as the pane/window command directly. - # The popup closes automatically when this script exits. - if set -q _flag_tmux - switch "$key" - case "alt-h" - tmux split-window -h -c "#{pane_current_path}" "distrobox enter $container_name" - case "alt-v" - tmux split-window -v -c "#{pane_current_path}" "distrobox enter $container_name" - case "" - tmux new-window -n "$container_name" "distrobox enter $container_name" - end - return 0 - end - - # ── zellij multi-action mode ───────────────────────────────────── - # Picker pane has close_on_exit=true, so it auto-closes when script exits. - # All actions launch distrobox as the pane's command directly. - if set -q _flag_zellij - switch "$key" - case "alt-s" - # Tiled split running distrobox directly - zellij action toggle-floating-panes - zellij action new-pane -d down --name "$container_name" -- distrobox enter $container_name - case "alt-t" - # New tab via temp layout with UI chrome - set -l layout_file (mktemp /tmp/zellij-container-XXXXXX.kdl) - printf 'layout { - pane size=1 borderless=true { - plugin location="compact-bar" - } - pane command="distrobox" { - args "enter" "%s" - name "%s" - } - pane size=1 borderless=true { - plugin location="status-bar" - } -}\n' $container_name $container_name > $layout_file - zellij action new-tab --layout "$layout_file" --name "$container_name" - rm -f "$layout_file" - case "" - # Floating pane running distrobox directly - zellij action new-pane --floating --name "$container_name" -- distrobox enter $container_name - end - return 0 - end - - # Default (no multiplexer flag): enter the container in the current shell - distrobox enter $container_name -end diff --git a/dot_config/tmux/scripts/executable_container-pick-send.fish b/dot_config/tmux/scripts/executable_container-pick-send.fish deleted file mode 100644 index e5262f7..0000000 --- a/dot_config/tmux/scripts/executable_container-pick-send.fish +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env fish -# container-pick-send.fish — tmux popup wrapper for container-fzf -# Runs the multi-action container picker inside a tmux popup. -# The popup provides the TTY that fzf needs. - -container-fzf --tmux diff --git a/dot_config/tmux/scripts/executable_container-pick-send.sh b/dot_config/tmux/scripts/executable_container-pick-send.sh new file mode 100644 index 0000000..230a21f --- /dev/null +++ b/dot_config/tmux/scripts/executable_container-pick-send.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# container-pick-send.sh — tmux popup wrapper for container-fzf +# Runs the multi-action container picker inside a tmux popup. +# The popup provides the TTY that fzf needs. + +exec container-fzf --tmux diff --git a/dot_config/tmux/tmux.conf.tmpl b/dot_config/tmux/tmux.conf.tmpl index cfeb589..5dac6c8 100644 --- a/dot_config/tmux/tmux.conf.tmpl +++ b/dot_config/tmux/tmux.conf.tmpl @@ -149,7 +149,7 @@ bind n new-session # ── Container Integration ───────────────────────────────────────────── # prefix+e: fzf container picker (enter=new window, alt-h=hsplit, alt-v=vsplit) -bind e display-popup -E -w 80% -h 60% "~/.config/tmux/scripts/container-pick-send.fish" +bind e display-popup -E -w 80% -h 60% "~/.config/tmux/scripts/container-pick-send.sh" # Container-aware splits: if current pane is inside a container, new splits # auto-enter the same container. Falls back to normal split on host. diff --git a/dot_config/zellij/config.kdl.tmpl b/dot_config/zellij/config.kdl.tmpl index a1c3228..0d6ac0a 100644 --- a/dot_config/zellij/config.kdl.tmpl +++ b/dot_config/zellij/config.kdl.tmpl @@ -96,9 +96,9 @@ keybinds clear-defaults=true { bind "w" { SearchToggleOption "Wrap"; } } session { - // Container picker: enter=current pane, alt-s=split, alt-t=new tab + // Container picker: enter=floating, alt-s=split, alt-t=new tab bind "e" { - Run "fish" "-c" "container-fzf --zellij" { + Run "container-fzf" "--zellij" { floating true close_on_exit true } diff --git a/dot_local/bin/executable_container-fzf b/dot_local/bin/executable_container-fzf new file mode 100644 index 0000000..63c260f --- /dev/null +++ b/dot_local/bin/executable_container-fzf @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +# container-fzf: Interactive container picker using fzf +# Lists distrobox containers and enters the selected one. +# Works from any shell. Called standalone or by tmux/zellij integrations. +# +# Flags: +# --tmux tmux mode: enter=new window, alt-h=hsplit, alt-v=vsplit +# --zellij zellij mode: enter=floating, alt-s=split, alt-t=new tab +# --print-only Print the enter command instead of executing it +# --running-only Only show running containers +# --header TEXT Override the fzf header text + +set -euo pipefail + +# ── Parse flags ────────────────────────────────────────────────────── +mode="" +print_only=0 +running_only=0 +custom_header="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --tmux) mode="tmux"; shift ;; + --zellij) mode="zellij"; shift ;; + --print-only) print_only=1; shift ;; + --running-only) running_only=1; shift ;; + --header) custom_header="$2"; shift 2 ;; + *) echo "container-fzf: unknown flag: $1" >&2; exit 1 ;; + esac +done + +# ── Guards ─────────────────────────────────────────────────────────── +if ! command -v distrobox >/dev/null 2>&1; then + echo "container-fzf: distrobox not found" >&2 + exit 1 +fi +if ! command -v fzf >/dev/null 2>&1; then + echo "container-fzf: fzf not found" >&2 + exit 1 +fi + +# ── Header text ────────────────────────────────────────────────────── +if [[ -n "$custom_header" ]]; then + fzf_header="$custom_header" +elif [[ "$mode" == "tmux" ]]; then + fzf_header="enter: new window | alt-h: hsplit | alt-v: vsplit" +elif [[ "$mode" == "zellij" ]]; then + fzf_header="enter: floating | alt-s: split | alt-t: new tab" +else + fzf_header="Select container" +fi + +# ── Get container list ─────────────────────────────────────────────── +db_output=$(distrobox list --no-color 2>/dev/null | tail -n +2) + +if [[ -z "$db_output" ]]; then + echo "container-fzf: no distrobox containers found" >&2 + echo "Create one with: distrobox create --name --image " >&2 + exit 1 +fi + +# Filter to running only if requested +if [[ "$running_only" -eq 1 ]]; then + db_output=$(echo "$db_output" | grep 'Up' || true) + if [[ -z "$db_output" ]]; then + echo "container-fzf: no running containers" >&2 + exit 1 + fi +fi + +# ── Build fzf command ──────────────────────────────────────────────── +fzf_args=( + --header "$fzf_header" + --preview 'sh -c '\''name=$(echo "$1" | awk -F"|" "{print \$2}" | xargs); echo "$1"; echo "---"; podman inspect --format "Image: {{.Config.Image}}\nCreated: {{.Created}}\nState: {{.State.Status}}" "$name" 2>/dev/null || echo "Container not running - will start on enter"'\'' -- {}' + --prompt "container> " + --layout reverse + --border rounded + --ansi +) + +# Multi-action modes use --expect to capture which key was pressed +if [[ "$mode" == "tmux" ]]; then + fzf_args+=(--expect "alt-h,alt-v") +elif [[ "$mode" == "zellij" ]]; then + fzf_args+=(--expect "alt-s,alt-t") +fi + +# ── Run fzf ────────────────────────────────────────────────────────── +fzf_output=$(echo "$db_output" | fzf "${fzf_args[@]}") || exit 0 + +if [[ -z "$fzf_output" ]]; then + exit 0 +fi + +# With --expect, first line is the key pressed, second is the selection +# Without --expect, the only line is the selection +key="" +selected="" +if [[ -n "$mode" ]]; then + key=$(echo "$fzf_output" | head -1) + selected=$(echo "$fzf_output" | tail -1) +else + selected="$fzf_output" +fi + +if [[ -z "$selected" ]]; then + exit 0 +fi + +# Extract container name (second column, pipe-delimited) +container_name=$(echo "$selected" | awk -F'|' '{print $2}' | xargs) + +if [[ -z "$container_name" ]]; then + echo "container-fzf: could not parse container name" >&2 + exit 1 +fi + +# ── --print-only ───────────────────────────────────────────────────── +if [[ "$print_only" -eq 1 ]]; then + echo "distrobox enter $container_name" + exit 0 +fi + +# ── tmux multi-action mode ─────────────────────────────────────────── +# All actions run distrobox as the pane/window command directly. +# The popup closes automatically when this script exits. +if [[ "$mode" == "tmux" ]]; then + case "$key" in + alt-h) tmux split-window -h "distrobox enter $container_name" ;; + alt-v) tmux split-window -v "distrobox enter $container_name" ;; + *) tmux new-window -n "$container_name" "distrobox enter $container_name" ;; + esac + exit 0 +fi + +# ── zellij multi-action mode ──────────────────────────────────────── +# Picker pane has close_on_exit=true, so it auto-closes when script exits. +# All actions launch distrobox as the pane's command directly. +if [[ "$mode" == "zellij" ]]; then + case "$key" in + alt-s) + # Tiled split running distrobox directly + zellij action toggle-floating-panes + zellij action new-pane -d down --name "$container_name" -- distrobox enter "$container_name" + ;; + alt-t) + # New tab via temp layout with UI chrome + layout_file=$(mktemp /tmp/zellij-container-XXXXXX.kdl) + cat > "$layout_file" << LAYOUT +layout { + pane size=1 borderless=true { + plugin location="compact-bar" + } + pane command="distrobox" { + args "enter" "$container_name" + name "$container_name" + } + pane size=1 borderless=true { + plugin location="status-bar" + } +} +LAYOUT + zellij action new-tab --layout "$layout_file" --name "$container_name" + rm -f "$layout_file" + ;; + *) + # Floating pane running distrobox directly + zellij action new-pane --floating --name "$container_name" -- distrobox enter "$container_name" + ;; + esac + exit 0 +fi + +# ── Default (no multiplexer flag) ──────────────────────────────────── +# Enter the container in the current shell +exec distrobox enter "$container_name" From 72552ee1d4fb5f0c1c3944d398ba0ffcf161a666 Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Sat, 28 Feb 2026 21:37:05 -0800 Subject: [PATCH 08/10] Converge container picker keybinds between tmux and zellij Unify hotkeys so both multiplexers use the same mental model: - Enter = primary action (new window in tmux, new tab in zellij) - Alt-s = split pane - Alt-f = floating pane (zellij only, N/A for tmux) Replaces the old alt-h/alt-v (tmux) and alt-t (zellij) keybinds. --- dot_config/zellij/config.kdl.tmpl | 2 +- dot_local/bin/executable_container-fzf | 27 +++++++++++++------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/dot_config/zellij/config.kdl.tmpl b/dot_config/zellij/config.kdl.tmpl index 0d6ac0a..d7beed3 100644 --- a/dot_config/zellij/config.kdl.tmpl +++ b/dot_config/zellij/config.kdl.tmpl @@ -96,7 +96,7 @@ keybinds clear-defaults=true { bind "w" { SearchToggleOption "Wrap"; } } session { - // Container picker: enter=floating, alt-s=split, alt-t=new tab + // Container picker: enter=new tab, alt-s=split, alt-f=floating bind "e" { Run "container-fzf" "--zellij" { floating true diff --git a/dot_local/bin/executable_container-fzf b/dot_local/bin/executable_container-fzf index 63c260f..fcc51aa 100644 --- a/dot_local/bin/executable_container-fzf +++ b/dot_local/bin/executable_container-fzf @@ -4,8 +4,8 @@ # Works from any shell. Called standalone or by tmux/zellij integrations. # # Flags: -# --tmux tmux mode: enter=new window, alt-h=hsplit, alt-v=vsplit -# --zellij zellij mode: enter=floating, alt-s=split, alt-t=new tab +# --tmux tmux mode: enter=new window, alt-s=split, alt-f=N/A +# --zellij zellij mode: enter=new tab, alt-s=split, alt-f=floating # --print-only Print the enter command instead of executing it # --running-only Only show running containers # --header TEXT Override the fzf header text @@ -43,9 +43,9 @@ fi if [[ -n "$custom_header" ]]; then fzf_header="$custom_header" elif [[ "$mode" == "tmux" ]]; then - fzf_header="enter: new window | alt-h: hsplit | alt-v: vsplit" + fzf_header="enter: new window | alt-s: split" elif [[ "$mode" == "zellij" ]]; then - fzf_header="enter: floating | alt-s: split | alt-t: new tab" + fzf_header="enter: new tab | alt-s: split | alt-f: floating" else fzf_header="Select container" fi @@ -80,9 +80,9 @@ fzf_args=( # Multi-action modes use --expect to capture which key was pressed if [[ "$mode" == "tmux" ]]; then - fzf_args+=(--expect "alt-h,alt-v") + fzf_args+=(--expect "alt-s") elif [[ "$mode" == "zellij" ]]; then - fzf_args+=(--expect "alt-s,alt-t") + fzf_args+=(--expect "alt-s,alt-f") fi # ── Run fzf ────────────────────────────────────────────────────────── @@ -126,8 +126,7 @@ fi # The popup closes automatically when this script exits. if [[ "$mode" == "tmux" ]]; then case "$key" in - alt-h) tmux split-window -h "distrobox enter $container_name" ;; - alt-v) tmux split-window -v "distrobox enter $container_name" ;; + alt-s) tmux split-window -v "distrobox enter $container_name" ;; *) tmux new-window -n "$container_name" "distrobox enter $container_name" ;; esac exit 0 @@ -143,8 +142,12 @@ if [[ "$mode" == "zellij" ]]; then zellij action toggle-floating-panes zellij action new-pane -d down --name "$container_name" -- distrobox enter "$container_name" ;; - alt-t) - # New tab via temp layout with UI chrome + alt-f) + # Floating pane running distrobox directly + zellij action new-pane --floating --name "$container_name" -- distrobox enter "$container_name" + ;; + *) + # New tab via temp layout with UI chrome (Enter key) layout_file=$(mktemp /tmp/zellij-container-XXXXXX.kdl) cat > "$layout_file" << LAYOUT layout { @@ -163,10 +166,6 @@ LAYOUT zellij action new-tab --layout "$layout_file" --name "$container_name" rm -f "$layout_file" ;; - *) - # Floating pane running distrobox directly - zellij action new-pane --floating --name "$container_name" -- distrobox enter "$container_name" - ;; esac exit 0 fi From 2b98837875cb39c47746d628fe20b3bd89a21c62 Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Sat, 28 Feb 2026 21:43:04 -0800 Subject: [PATCH 09/10] Drop alt-f floating action from zellij container picker Alt+f is already bound to ToggleFloatingPanes in zellij's shared_except keybinds, so it never reaches fzf. Users can toggle floating with Alt+f after opening a container pane. --- dot_config/zellij/config.kdl.tmpl | 2 +- dot_local/bin/executable_container-fzf | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/dot_config/zellij/config.kdl.tmpl b/dot_config/zellij/config.kdl.tmpl index d7beed3..eabfba9 100644 --- a/dot_config/zellij/config.kdl.tmpl +++ b/dot_config/zellij/config.kdl.tmpl @@ -96,7 +96,7 @@ keybinds clear-defaults=true { bind "w" { SearchToggleOption "Wrap"; } } session { - // Container picker: enter=new tab, alt-s=split, alt-f=floating + // Container picker: enter=new tab, alt-s=split bind "e" { Run "container-fzf" "--zellij" { floating true diff --git a/dot_local/bin/executable_container-fzf b/dot_local/bin/executable_container-fzf index fcc51aa..1c8b27c 100644 --- a/dot_local/bin/executable_container-fzf +++ b/dot_local/bin/executable_container-fzf @@ -5,7 +5,7 @@ # # Flags: # --tmux tmux mode: enter=new window, alt-s=split, alt-f=N/A -# --zellij zellij mode: enter=new tab, alt-s=split, alt-f=floating +# --zellij zellij mode: enter=new tab, alt-s=split # --print-only Print the enter command instead of executing it # --running-only Only show running containers # --header TEXT Override the fzf header text @@ -45,7 +45,7 @@ if [[ -n "$custom_header" ]]; then elif [[ "$mode" == "tmux" ]]; then fzf_header="enter: new window | alt-s: split" elif [[ "$mode" == "zellij" ]]; then - fzf_header="enter: new tab | alt-s: split | alt-f: floating" + fzf_header="enter: new tab | alt-s: split" else fzf_header="Select container" fi @@ -82,7 +82,7 @@ fzf_args=( if [[ "$mode" == "tmux" ]]; then fzf_args+=(--expect "alt-s") elif [[ "$mode" == "zellij" ]]; then - fzf_args+=(--expect "alt-s,alt-f") + fzf_args+=(--expect "alt-s") fi # ── Run fzf ────────────────────────────────────────────────────────── @@ -142,10 +142,6 @@ if [[ "$mode" == "zellij" ]]; then zellij action toggle-floating-panes zellij action new-pane -d down --name "$container_name" -- distrobox enter "$container_name" ;; - alt-f) - # Floating pane running distrobox directly - zellij action new-pane --floating --name "$container_name" -- distrobox enter "$container_name" - ;; *) # New tab via temp layout with UI chrome (Enter key) layout_file=$(mktemp /tmp/zellij-container-XXXXXX.kdl) From bf1fd5e289ea6c964a4bc945c7d8680472818fbd Mon Sep 17 00:00:00 2001 From: Ty Smith Date: Sat, 28 Feb 2026 21:47:25 -0800 Subject: [PATCH 10/10] Fix PR review issues: grep+set -e, command injection, stale bindings - container-split.sh / container-status.sh: replace grep with sed -n to avoid exit code 1 under set -euo pipefail when no match found; add || true to pgrep calls - container-split.sh: sanitize CONTAINER_ID against [A-Za-z0-9._-]+ before interpolation to prevent command injection from inside containers; pass distrobox as separate args instead of single shell string - tmux.conf.tmpl: remove duplicate prefix+h/v plain split bindings (overridden by container-aware splits below) - Fix stale comments in tmux.conf.tmpl and container.kdl --- dot_config/tmux/scripts/executable_container-split.sh | 11 ++++++++--- .../tmux/scripts/executable_container-status.sh | 4 ++-- dot_config/tmux/tmux.conf.tmpl | 6 ++---- dot_config/zellij/layouts/container.kdl | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/dot_config/tmux/scripts/executable_container-split.sh b/dot_config/tmux/scripts/executable_container-split.sh index 9d2e9d8..1fe0c14 100755 --- a/dot_config/tmux/scripts/executable_container-split.sh +++ b/dot_config/tmux/scripts/executable_container-split.sh @@ -19,9 +19,9 @@ CONTAINER_ID="" # Walk the process tree to find a shell that has CONTAINER_ID set if [ -n "$PANE_PID" ]; then # Try to read CONTAINER_ID from any child process of the pane - for pid in $(pgrep -P "$PANE_PID" 2>/dev/null) $PANE_PID; do + for pid in $(pgrep -P "$PANE_PID" 2>/dev/null || true) $PANE_PID; do if [ -r "/proc/$pid/environ" ]; then - cid=$(tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | grep '^CONTAINER_ID=' | head -1 | cut -d= -f2-) + cid=$(tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | sed -n 's/^CONTAINER_ID=//p' | head -1) if [ -n "$cid" ]; then CONTAINER_ID="$cid" break @@ -31,8 +31,13 @@ if [ -n "$PANE_PID" ]; then fi if [ -n "$CONTAINER_ID" ]; then + # Sanitize: only allow alphanumeric, dash, underscore, dot + if ! [[ "$CONTAINER_ID" =~ ^[A-Za-z0-9._-]+$ ]]; then + echo "container-split.sh: invalid CONTAINER_ID: $CONTAINER_ID" >&2 + exit 1 + fi # Inside a container — new split enters the same container - tmux split-window "$SPLIT_DIR" -c "#{pane_current_path}" "distrobox enter $CONTAINER_ID" + tmux split-window "$SPLIT_DIR" -c "#{pane_current_path}" -- distrobox enter "$CONTAINER_ID" else # On host — normal split tmux split-window "$SPLIT_DIR" -c "#{pane_current_path}" diff --git a/dot_config/tmux/scripts/executable_container-status.sh b/dot_config/tmux/scripts/executable_container-status.sh index e0a6c01..8299b06 100755 --- a/dot_config/tmux/scripts/executable_container-status.sh +++ b/dot_config/tmux/scripts/executable_container-status.sh @@ -9,9 +9,9 @@ PANE_PID=$(tmux display-message -p '#{pane_pid}' 2>/dev/null) [ -z "$PANE_PID" ] && exit 0 # Walk the pane's process tree looking for CONTAINER_ID -for pid in $(pgrep -P "$PANE_PID" 2>/dev/null) $PANE_PID; do +for pid in $(pgrep -P "$PANE_PID" 2>/dev/null || true) $PANE_PID; do if [ -r "/proc/$pid/environ" ]; then - cid=$(tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | grep '^CONTAINER_ID=' | head -1 | cut -d= -f2-) + cid=$(tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | sed -n 's/^CONTAINER_ID=//p' | head -1) if [ -n "$cid" ]; then echo "󰏖 $cid" exit 0 diff --git a/dot_config/tmux/tmux.conf.tmpl b/dot_config/tmux/tmux.conf.tmpl index 5dac6c8..0382ceb 100644 --- a/dot_config/tmux/tmux.conf.tmpl +++ b/dot_config/tmux/tmux.conf.tmpl @@ -18,9 +18,7 @@ setw -g mode-keys vi bind -T copy-mode-vi v send -X begin-selection bind -T copy-mode-vi y send -X copy-selection-and-cancel -# Pane Controls -bind h split-window -h -c "#{pane_current_path}" -bind v split-window -v -c "#{pane_current_path}" +# Pane Controls (prefix+h/v are container-aware splits, see Container Integration below) bind -n C-M-PageUp split-window -h -c "#{pane_current_path}" bind -n C-M-PageDown split-window -v -c "#{pane_current_path}" bind -n C-M-Home split-window -h -c "#{pane_current_path}" @@ -148,7 +146,7 @@ bind n new-session # bind R source-file ~/.config/tmux/tmux.conf # ── Container Integration ───────────────────────────────────────────── -# prefix+e: fzf container picker (enter=new window, alt-h=hsplit, alt-v=vsplit) +# prefix+e: fzf container picker (enter=new window, alt-s=split) bind e display-popup -E -w 80% -h 60% "~/.config/tmux/scripts/container-pick-send.sh" # Container-aware splits: if current pane is inside a container, new splits diff --git a/dot_config/zellij/layouts/container.kdl b/dot_config/zellij/layouts/container.kdl index 9ca2776..bd790f1 100644 --- a/dot_config/zellij/layouts/container.kdl +++ b/dot_config/zellij/layouts/container.kdl @@ -2,7 +2,7 @@ // Usage: zellij --layout container // // Opens a host tab and a container picker tab. -// To create a layout with a specific container, use: +// To create a named session using this layout, use: // zellij --layout container --session my-project layout {