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/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.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/scripts/executable_container-split.sh b/dot_config/tmux/scripts/executable_container-split.sh new file mode 100755 index 0000000..1fe0c14 --- /dev/null +++ b/dot_config/tmux/scripts/executable_container-split.sh @@ -0,0 +1,44 @@ +#!/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 || true) $PANE_PID; do + if [ -r "/proc/$pid/environ" ]; then + 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 + fi + fi + done +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" +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..8299b06 --- /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 || true) $PANE_PID; do + if [ -r "/proc/$pid/environ" ]; then + 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 + fi + fi +done diff --git a/dot_config/tmux/tmux.conf.tmpl b/dot_config/tmux/tmux.conf.tmpl index ffb9326..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}" @@ -147,8 +145,18 @@ 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 +# ── Container Integration ───────────────────────────────────────────── +# 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 +# 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= # ── Plugins ────────────────────────────────────────────────────────── set-environment -g TMUX_PLUGIN_MANAGER_PATH '~/.config/tmux/plugins' @@ -189,13 +197,24 @@ 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}" +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}" +# ── 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 +226,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] " diff --git a/dot_config/zellij/config.kdl.tmpl b/dot_config/zellij/config.kdl.tmpl index d70b613..eabfba9 100644 --- a/dot_config/zellij/config.kdl.tmpl +++ b/dot_config/zellij/config.kdl.tmpl @@ -96,6 +96,14 @@ keybinds clear-defaults=true { bind "w" { SearchToggleOption "Wrap"; } } session { + // Container picker: enter=new tab, alt-s=split + bind "e" { + Run "container-fzf" "--zellij" { + floating true + close_on_exit true + } + 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..bd790f1 --- /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 named session using this layout, 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" + } + } +} diff --git a/dot_local/bin/executable_container-fzf b/dot_local/bin/executable_container-fzf new file mode 100644 index 0000000..1c8b27c --- /dev/null +++ b/dot_local/bin/executable_container-fzf @@ -0,0 +1,171 @@ +#!/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-s=split, alt-f=N/A +# --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 + +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-s: split" +elif [[ "$mode" == "zellij" ]]; then + fzf_header="enter: new tab | alt-s: split" +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-s") +elif [[ "$mode" == "zellij" ]]; then + fzf_args+=(--expect "alt-s") +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-s) 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" + ;; + *) + # New tab via temp layout with UI chrome (Enter key) + 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" + ;; + esac + exit 0 +fi + +# ── Default (no multiplexer flag) ──────────────────────────────────── +# Enter the container in the current shell +exec distrobox enter "$container_name"