From 6a122ec1a6cedccc309413f515504ebb83b92bb1 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Mon, 29 Jun 2026 11:19:08 -0300 Subject: [PATCH 1/6] feat: add zellij support to cc/oc helpers --- README.md | 25 +++++++ local/env.zsh.example | 4 ++ zsh/scripts/claude-helpers.zsh | 118 +++++++++++++++++++++++++++++++-- 3 files changed, 143 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 543c1a7..11aa82c 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ dotfiles/ | `cortex`, `dotfiles` | Navegación rápida al repo `cortex` y sus dotfiles | | `cc [path]` | Abrir Claude Code | | `oc [path]` | Abrir OpenCode | +| `zj [path]` | Entrar/crear sesión Zellij por repo/path | +| `zsessions` | Listar sesiones Zellij | | `ccclip ` | Copiar código al clipboard | | `tcc`, `tdev`, `ta`, `tn`, `tl`, `tk` | Helpers tmux (`tcc` abre Claude Code en tmux) | | `wtadd`, `wtlist`, `wtremove` | Helpers de git worktrees | @@ -98,9 +100,32 @@ Editá `local/env.zsh` (gitignored) para configurar: - `SCREENSHOTS_DIR` — directorio de screenshots - `WORKSPACE_DIR` — directorio raíz de tus proyectos - `OPENCODE_DEFAULT_FLAGS` — flags por defecto para `oc` +- `CORTEX_MULTIPLEXER=zellij` — usa Zellij para `cc`/`oc` fuera de cmux/tmux, recomendado en `agent-dev-01` - `INNIT_DIR` y overrides `INNIT_*_DIR` — navegación rápida de subdirectorios - Aliases y paths personales +## Zellij en agent-dev-01 + +En una workstation remota persistente, el modelo recomendado es una sesión Zellij por repo: + +```bash +zj ~/dev/personal/infra +zj ~/dev/personal/cortex +``` + +`cc [path]`, `ccb [path]`, `oc [path]` y `ocb [path]` usan Zellij cuando estás dentro de una sesión Zellij o cuando definís: + +```bash +export CORTEX_MULTIPLEXER="zellij" +``` + +Comportamiento: + +- Si estás en la sesión del repo actual, ejecuta el agente en el pane actual. +- Si pedís otro path y la sesión ya existe, cambia a esa sesión. +- Si pedís otro path y la sesión no existe, la crea con el agente arrancado en ese directorio. +- En macOS/cmux sin `CORTEX_MULTIPLEXER=zellij`, `cc`/`oc` conservan el comportamiento actual con cmux/tmux. + ## SketchyBar La config macOS enlaza `sketchybar/` en `~/.config/sketchybar`. El diseño es sobrio, notch-safe y usa la paleta dark/green de InnIT. diff --git a/local/env.zsh.example b/local/env.zsh.example index 4334f9c..29dc473 100644 --- a/local/env.zsh.example +++ b/local/env.zsh.example @@ -16,6 +16,10 @@ # OpenCode # export OPENCODE_DEFAULT_FLAGS="" +# Multiplexor preferido para cc/oc cuando no estás en cmux. +# En agent-dev-01 conviene zellij para sesiones persistentes por repo. +# export CORTEX_MULTIPLEXER="zellij" + # Navegación rápida para proyectos de una organización/equipo # export INNIT_DIR="$WORKSPACE_DIR/innit" # export INNIT_APIS_DIR="$INNIT_DIR/apis" diff --git a/zsh/scripts/claude-helpers.zsh b/zsh/scripts/claude-helpers.zsh index 0046750..29b9fed 100644 --- a/zsh/scripts/claude-helpers.zsh +++ b/zsh/scripts/claude-helpers.zsh @@ -26,6 +26,108 @@ _workspace_name_for_path() { fi } +_zellij_preferred() { + command -v zellij >/dev/null 2>&1 || return 1 + [[ -n "$ZELLIJ" || "${CORTEX_MULTIPLEXER:-}" == "zellij" ]] +} + +_zellij_session_exists() { + local session="$1" + zellij list-sessions 2>/dev/null | awk '{print $1}' | grep -Fxq "$session" +} + +_zellij_kdl_escape() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//\"/\\\"}" + printf '%s' "$value" +} + +_zellij_layout_for_command() { + local resolved="$1" + local command_line="$2" + local layout_file + layout_file=$(mktemp "${TMPDIR:-/tmp}/cortex-zellij-layout.XXXXXX.kdl") + + local cwd_escaped command_escaped shell_name + cwd_escaped="$(_zellij_kdl_escape "$resolved")" + command_escaped="$(_zellij_kdl_escape "cd ${(q)resolved} && $command_line; exec ${SHELL:-zsh}")" + shell_name="$(_zellij_kdl_escape "${SHELL:-zsh}")" + + cat > "$layout_file" </dev/null && pwd) + + if [[ -z "$resolved" ]]; then + echo "❌ Directorio no encontrado: $target" + return 1 + fi + + if ! command -v zellij >/dev/null 2>&1; then + echo "❌ zellij no está instalado" + return 1 + fi + + local session + session="$(_workspace_name_for_path "$resolved")" + + if [[ -n "$ZELLIJ" ]]; then + zellij action switch-session -c "$resolved" "$session" + else + cd "$resolved" && zellij attach "$session" --create + fi +} + +zsessions() { + zellij list-sessions +} + _cmux_rename_workspace() { local workspace_id="$1" local workspace_name="$2" @@ -54,7 +156,9 @@ cc() { return 1 fi - if [[ -n "$CMUX_WORKSPACE_ID" ]]; then + if _zellij_preferred; then + _zellij_open_agent "$resolved" "claude --enable-auto-mode --dangerously-skip-permissions" + elif [[ -n "$CMUX_WORKSPACE_ID" ]]; then # Estamos dentro de cmux local workspace_name workspace_name="$(_workspace_name_for_path "$resolved")" @@ -117,7 +221,9 @@ oc() { local oc_cmd="opencode ${OPENCODE_DEFAULT_FLAGS:-}" - if [[ -n "$CMUX_WORKSPACE_ID" ]]; then + if _zellij_preferred; then + _zellij_open_agent "$resolved" "$oc_cmd" + elif [[ -n "$CMUX_WORKSPACE_ID" ]]; then local workspace_name workspace_name="$(_workspace_name_for_path "$resolved")" @@ -173,7 +279,9 @@ ccb() { local cc_cmd="claude --dangerously-skip-permissions" - if [[ -n "$CMUX_WORKSPACE_ID" ]]; then + if _zellij_preferred; then + _zellij_open_agent "$resolved" "$cc_cmd" + elif [[ -n "$CMUX_WORKSPACE_ID" ]]; then local workspace_name workspace_name="$(_workspace_name_for_path "$resolved")" @@ -229,7 +337,9 @@ ocb() { local oc_cmd="opencode ${OPENCODE_DEFAULT_FLAGS:-}" - if [[ -n "$CMUX_WORKSPACE_ID" ]]; then + if _zellij_preferred; then + _zellij_open_agent "$resolved" "$oc_cmd" + elif [[ -n "$CMUX_WORKSPACE_ID" ]]; then local workspace_name workspace_name="$(_workspace_name_for_path "$resolved")" From 9944984b52e1400961fb7c5b54df070b4f553673 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Mon, 29 Jun 2026 11:25:24 -0300 Subject: [PATCH 2/6] fix: avoid zsh int cast in welcome timer --- zsh/zshrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zsh/zshrc b/zsh/zshrc index 34bfb23..aa67a58 100644 --- a/zsh/zshrc +++ b/zsh/zshrc @@ -245,7 +245,8 @@ fi #region Welcome _welcome() { - local ms=$(( int(($EPOCHREALTIME - _PROFILE_START) * 1000) )) + local ms + ms=$(awk -v start="${_PROFILE_START:-0}" -v now="${EPOCHREALTIME:-0}" 'BEGIN { printf "%d", (now - start) * 1000 }' 2>/dev/null || printf "0") echo "" echo " ╔══════════════════════════════════════╗" From cbcfff0681ec13800d96d751b2d77e85d24239a9 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Mon, 29 Jun 2026 21:03:41 -0300 Subject: [PATCH 3/6] feat: tune Ghostty and Zellij defaults --- README.md | 39 ++++---- ghostty/config | 3 +- install.sh | 21 ++--- local/env.zsh.example | 3 +- zellij/config.kdl | 35 ++++++++ zellij/layouts/innit.kdl | 15 ++++ zsh/scripts/claude-helpers.zsh | 148 +++++++++++++++++++------------ zsh/scripts/tmux-helpers.zsh | 43 +++++---- zsh/scripts/worktree-helpers.zsh | 10 +-- zsh/zshrc | 21 +++-- 10 files changed, 215 insertions(+), 123 deletions(-) create mode 100644 zellij/config.kdl create mode 100644 zellij/layouts/innit.kdl diff --git a/README.md b/README.md index 11aa82c..0664feb 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,17 @@ Configuración local extraida de `cortex`: terminal, shell, prompt, helpers de A ## Stack -- **Terminal**: [Ghostty](https://ghostty.org/) y Alacritty +- **Terminal**: [Ghostty](https://ghostty.org/) - **Shell**: Zsh nativo de macOS - **Prompt**: [Starship](https://starship.rs/) — tema Gruvbox Dark -- **Multiplexor**: tmux + helpers de sesión +- **Multiplexor**: [Zellij](https://zellij.dev/) + helpers de sesión - **Barra macOS**: [SketchyBar](https://github.com/FelixKratz/SketchyBar) con tema Gruvbox - **Window manager macOS**: [yabai](https://github.com/koekeishiya/yabai) + [skhd](https://github.com/koekeishiya/skhd) opcional y gradual - **Keyboard remaps macOS**: [Karabiner-Elements](https://karabiner-elements.pqrs.org/) con profile `cortex` - **Editor terminal**: [micro](https://micro-editor.github.io/) - **Editor principal**: Neovim basado en LazyVim/Gentleman.Dots con overlay RefactorIA - **Ls**: [eza](https://github.com/eza-community/eza) -- **AI CLI UX**: Claude Code statusline, OpenCode helpers y cmux hooks opcionales +- **AI CLI UX**: Claude Code statusline, OpenCode helpers y sesiones Zellij por repo - **Supply-chain guardrails**: defaults globales para `uv`, `npm`, `pnpm` y `bun` - **Fuente**: FiraCode Nerd Font + variante custom RefactorIA @@ -29,7 +29,7 @@ bash install.sh ``` El instalador macOS: -1. Instala dependencias via Homebrew (starship, tmux, lazygit, micro, eza, sketchybar, yabai, skhd, Karabiner-Elements, FiraCode Nerd Font) +1. Instala dependencias via Homebrew (starship, zellij, lazygit, micro, eza, sketchybar, yabai, skhd, Karabiner-Elements, FiraCode Nerd Font) 2. Hace backup de configs existentes con timestamp 3. Crea symlinks de los dotfiles y guardrails globales (`.npmrc`, `pnpm/rc`, `.bunfig.toml`, `uv.toml`) 4. Intenta seleccionar el profile `cortex` de Karabiner si `karabiner_cli` está disponible @@ -41,8 +41,9 @@ El instalador macOS: ``` dotfiles/ ├── claude/ # Claude Code statusline -├── ghostty/ # Config Ghostty, cmux Ghostty config, muxy legado y shaders +├── ghostty/ # Config Ghostty y shaders ├── fonts/ # Fuente RefactorIA y script de regeneración +├── zellij/ # Theme/layout Zellij InnIT ├── npm/ # Global npm defaults (~/.npmrc) ├── pnpm/ # Global pnpm defaults (~/Library/Preferences/pnpm/rc) ├── bun/ # Global bun defaults (~/.bunfig.toml) @@ -52,11 +53,11 @@ dotfiles/ │ └── scripts/ │ ├── claude-helpers.zsh # Integración Claude Code │ ├── git-helpers.zsh # Identidades Git y clone helpers -│ ├── tmux-helpers.zsh # Helpers tmux +│ ├── tmux-helpers.zsh # Compat aliases t* sobre Zellij │ ├── worktree-helpers.zsh # Helpers git worktree │ ├── screenshots.zsh # Manejo de screenshots macOS │ └── pcsoft-helpers.zsh # Protección archivos PCSoft -├── tmux/ # Config tmux +├── tmux/ # Config tmux legacy, no enlazada por default ├── lazygit/ # Config lazygit ├── karabiner/ # Config Karabiner-Elements (~/.config/karabiner/karabiner.json) ├── micro/ # Settings y themes de micro @@ -84,7 +85,7 @@ dotfiles/ | `zj [path]` | Entrar/crear sesión Zellij por repo/path | | `zsessions` | Listar sesiones Zellij | | `ccclip ` | Copiar código al clipboard | -| `tcc`, `tdev`, `ta`, `tn`, `tl`, `tk` | Helpers tmux (`tcc` abre Claude Code en tmux) | +| `tcc`, `tdev`, `ta`, `tn`, `tl`, `tk` | Helpers Zellij compatibles con la memoria muscular tmux | | `wtadd`, `wtlist`, `wtremove` | Helpers de git worktrees | | `ss [n]` | Listar últimos screenshots | | `last [-c\|-o]` | Último screenshot | @@ -100,20 +101,20 @@ Editá `local/env.zsh` (gitignored) para configurar: - `SCREENSHOTS_DIR` — directorio de screenshots - `WORKSPACE_DIR` — directorio raíz de tus proyectos - `OPENCODE_DEFAULT_FLAGS` — flags por defecto para `oc` -- `CORTEX_MULTIPLEXER=zellij` — usa Zellij para `cc`/`oc` fuera de cmux/tmux, recomendado en `agent-dev-01` +- `CORTEX_MULTIPLEXER=zellij` — usa Zellij para `cc`/`oc`; es el default del profile - `INNIT_DIR` y overrides `INNIT_*_DIR` — navegación rápida de subdirectorios - Aliases y paths personales -## Zellij en agent-dev-01 +## Zellij -En una workstation remota persistente, el modelo recomendado es una sesión Zellij por repo: +El modelo recomendado es una sesión Zellij por repo: ```bash zj ~/dev/personal/infra zj ~/dev/personal/cortex ``` -`cc [path]`, `ccb [path]`, `oc [path]` y `ocb [path]` usan Zellij cuando estás dentro de una sesión Zellij o cuando definís: +`cc [path]`, `ccb [path]`, `oc [path]` y `ocb [path]` usan Zellij por default: ```bash export CORTEX_MULTIPLEXER="zellij" @@ -124,7 +125,9 @@ Comportamiento: - Si estás en la sesión del repo actual, ejecuta el agente en el pane actual. - Si pedís otro path y la sesión ya existe, cambia a esa sesión. - Si pedís otro path y la sesión no existe, la crea con el agente arrancado en ese directorio. -- En macOS/cmux sin `CORTEX_MULTIPLEXER=zellij`, `cc`/`oc` conservan el comportamiento actual con cmux/tmux. +- Las sesiones se nombran con contexto visible: `local::` o `ssh::`. +- El layout `innit` muestra tab bar arriba y status bar abajo usando el theme `innit`. +- Si necesitás evitar Zellij puntualmente, seteá `CORTEX_MULTIPLEXER` vacío en esa shell y ejecutá el agente directo. ## SketchyBar @@ -135,9 +138,9 @@ Layout activo: | Pantalla | Uso | Layout | |------|-----|--------| | Mac Retina (`display=1`) | apps generales: Discord, WhatsApp, Mail, Postman, Zen Browser | app activa + network, volumen, calendario, hora, batería | -| ViewSonic vertical (`display=2`) | auxiliar/random, cmux y Claude de formato vertical | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | -| LG Ultrawide (`display=3`) | mixto: cmux, Claude, ChatGPT, Obsidian | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | -| 4K derecho (`display=4`) | cmux exclusivo | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | +| ViewSonic vertical (`display=2`) | auxiliar/random, Ghostty/Zellij y Claude de formato vertical | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | +| LG Ultrawide (`display=3`) | mixto: Ghostty/Zellij, Claude, ChatGPT, Obsidian | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | +| 4K derecho (`display=4`) | Ghostty/Zellij exclusivo | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | El centro queda libre para evitar el notch y reducir ruido visual. @@ -151,7 +154,7 @@ Interacciones: | Batería | abre Battery Settings | | Fecha/hora | abre Calendar | -La barra asume Mac con notch y varios monitores: el centro queda libre y los items operativos se mantienen en los laterales. Los items de contexto (`git`, issue/PR, SDD y timer) se actualizan con un agregador liviano que lee `${XDG_CACHE_HOME:-~/.cache}/cortex/active-workspace`. Un watcher de eventos `workspace.selected` de cmux actualiza ese archivo y refresca los items al cambiar de workspace. `SKETCHYBAR_WORKSPACE` permite forzar un repo específico. +La barra asume Mac con notch y varios monitores: el centro queda libre y los items operativos se mantienen en los laterales. Los items de contexto (`git`, issue/PR, SDD y timer) se actualizan con un agregador liviano que lee `${XDG_CACHE_HOME:-~/.cache}/cortex/active-workspace`. `SKETCHYBAR_WORKSPACE` permite forzar un repo específico. El layout cambia automáticamente al recargar SketchyBar: con un solo display, el Retina mantiene los indicadores de estado útiles; con varios displays, el Retina queda liviano y el layout completo se mueve al externo disponible. @@ -276,7 +279,7 @@ El prompt usa una variante local de FiraCode Nerd Font Mono con el glyph de la b | Codepoint | `U+F0F00` | | Glyph test | `python3 -c 'print("\U000F0F00")'` | -La config de Starship usa este glyph PUA directamente. Si la terminal no tiene seleccionada `FiraCode Nerd Font Mono Beard`, el prompt puede mostrar un cuadrado/tofu en lugar de la barba. En macOS, `install.sh` instala la fuente y deja configurados Ghostty y cmux con esa family. +La config de Starship usa este glyph PUA directamente. Si la terminal no tiene seleccionada `FiraCode Nerd Font Mono Beard`, el prompt puede mostrar un cuadrado/tofu en lugar de la barba. En macOS, `install.sh` instala la fuente y deja configurado Ghostty con esa family. Para regenerar la fuente: diff --git a/ghostty/config b/ghostty/config index 440013b..85f23c9 100644 --- a/ghostty/config +++ b/ghostty/config @@ -1,5 +1,6 @@ -# Fuente +# Fuente primaria: FiraCode Beard custom. Fallback: Nerd Font oficial para glyphs. font-family = FiraCode Nerd Font Mono Beard +font-family = FiraCode Nerd Font font-size = 14 # Paleta Dark Vibrant (portada desde Windows Terminal) diff --git a/install.sh b/install.sh index f291941..55dc5e7 100755 --- a/install.sh +++ b/install.sh @@ -55,11 +55,11 @@ else echo " ✓ eza ya instalado" fi -if ! command -v tmux &>/dev/null; then - echo " → Instalando tmux (multiplexor de terminal)..." - brew install tmux +if ! command -v zellij &>/dev/null; then + echo " → Instalando zellij (multiplexor de terminal)..." + brew install zellij else - echo " ✓ tmux ya instalado" + echo " ✓ zellij ya instalado" fi if ! command -v lazygit &>/dev/null; then @@ -148,8 +148,8 @@ backup_if_exists "$HOME/.bunfig.toml" backup_if_exists "$HOME/.config/uv/uv.toml" backup_if_exists "$HOME/.config/starship.toml" backup_if_exists "$HOME/.config/ghostty/config" -backup_if_exists "$HOME/Library/Application Support/com.cmuxterm.app/config.ghostty" -backup_if_exists "$HOME/.tmux.conf" +backup_if_exists "$HOME/.config/zellij/config.kdl" +backup_if_exists "$HOME/.config/zellij/layouts/innit.kdl" backup_if_exists "$HOME/.claude/statusline.sh" backup_if_exists "$HOME/.config/lazygit/config.yml" backup_if_exists "$HOME/.config/sketchybar" @@ -180,9 +180,9 @@ create_symlink "$DOTFILES/bun/bunfig.toml" "$HOME/.bunfig.toml" create_symlink "$DOTFILES/uv/uv.toml" "$HOME/.config/uv/uv.toml" create_symlink "$DOTFILES/starship/starship.toml" "$HOME/.config/starship.toml" create_symlink "$DOTFILES/ghostty/config" "$HOME/.config/ghostty/config" -create_symlink "$DOTFILES/ghostty/cmux.conf" "$HOME/Library/Application Support/com.cmuxterm.app/config.ghostty" create_symlink "$DOTFILES/ghostty/shaders" "$HOME/.config/ghostty/shaders" -create_symlink "$DOTFILES/tmux/tmux.conf" "$HOME/.tmux.conf" +create_symlink "$DOTFILES/zellij/config.kdl" "$HOME/.config/zellij/config.kdl" +create_symlink "$DOTFILES/zellij/layouts/innit.kdl" "$HOME/.config/zellij/layouts/innit.kdl" chmod +x "$DOTFILES/claude/statusline.sh" create_symlink "$DOTFILES/claude/statusline.sh" "$HOME/.claude/statusline.sh" create_symlink "$DOTFILES/micro/settings.json" "$HOME/.config/micro/settings.json" @@ -282,8 +282,9 @@ echo " Próximos pasos:" echo " 1. Abrí una nueva tab en Ghostty para cargar el nuevo profile" echo " 2. Editá local/env.zsh con tus paths personales" echo " 3. Ghostty ya usa FiraCode Nerd Font Mono Beard (reiniciá si no se ve bien)" -echo " 4. Si macOS bloqueó servicios, habilitá Accessibility y corré los fallbacks impresos arriba" -echo " 5. Abrí Karabiner-Elements y habilitá Input Monitoring/Accessibility si macOS lo pide" +echo " 4. Usá zj, cc u oc para abrir sesiones Zellij por repo" +echo " 5. Si macOS bloqueó servicios, habilitá Accessibility y corré los fallbacks impresos arriba" +echo " 6. Abrí Karabiner-Elements y habilitá Input Monitoring/Accessibility si macOS lo pide" echo "" echo " Para medir el load time:" echo " \$ time zsh -i -c exit" diff --git a/local/env.zsh.example b/local/env.zsh.example index 29dc473..d48a36a 100644 --- a/local/env.zsh.example +++ b/local/env.zsh.example @@ -16,8 +16,7 @@ # OpenCode # export OPENCODE_DEFAULT_FLAGS="" -# Multiplexor preferido para cc/oc cuando no estás en cmux. -# En agent-dev-01 conviene zellij para sesiones persistentes por repo. +# Multiplexor preferido para cc/oc. Zellij es el default del profile. # export CORTEX_MULTIPLEXER="zellij" # Navegación rápida para proyectos de una organización/equipo diff --git a/zellij/config.kdl b/zellij/config.kdl new file mode 100644 index 0000000..f2600ce --- /dev/null +++ b/zellij/config.kdl @@ -0,0 +1,35 @@ +theme "innit" + +themes { + innit { + fg 248 250 252 + bg 11 17 24 + black 0 0 0 + red 239 68 68 + green 63 185 80 + yellow 245 158 11 + blue 56 189 248 + magenta 160 32 240 + cyan 58 150 221 + white 248 250 252 + orange 245 158 11 + + ribbon_selected { + base 248 250 252 + background 63 185 80 + emphasis_0 11 17 24 + emphasis_1 56 189 248 + emphasis_2 245 158 11 + emphasis_3 239 68 68 + } + + ribbon_unselected { + base 148 163 184 + background 16 24 32 + emphasis_0 63 185 80 + emphasis_1 56 189 248 + emphasis_2 245 158 11 + emphasis_3 239 68 68 + } + } +} diff --git a/zellij/layouts/innit.kdl b/zellij/layouts/innit.kdl new file mode 100644 index 0000000..e02e015 --- /dev/null +++ b/zellij/layouts/innit.kdl @@ -0,0 +1,15 @@ +layout { + default_tab_template { + pane size=1 borderless=true { + plugin location="zellij:tab-bar" + } + children + pane size=2 borderless=true { + plugin location="zellij:status-bar" + } + } + + tab name="shell" focus=true { + pane + } +} diff --git a/zsh/scripts/claude-helpers.zsh b/zsh/scripts/claude-helpers.zsh index 29b9fed..86c25ad 100644 --- a/zsh/scripts/claude-helpers.zsh +++ b/zsh/scripts/claude-helpers.zsh @@ -26,11 +26,35 @@ _workspace_name_for_path() { fi } +_zellij_context_label() { + local host + host="${HOST%%.*}" + host="${host:-$(hostname -s 2>/dev/null)}" + + if [[ -n "$SSH_CONNECTION" || -n "$SSH_CLIENT" || -n "$SSH_TTY" ]]; then + printf 'ssh:%s' "$host" + else + printf 'local:%s' "$host" + fi +} + +_zellij_session_name_for_path() { + printf '%s:%s' "$(_zellij_context_label)" "$(_workspace_name_for_path "${1:-$PWD}")" +} + +_zellij_default_layout() { + local layout_file="${_DOTFILES_DIR:-$HOME/dev/personal/cortex-dotfiles}/zellij/layouts/innit.kdl" + [[ -f "$layout_file" ]] && printf '%s' "$layout_file" +} + _zellij_preferred() { - command -v zellij >/dev/null 2>&1 || return 1 [[ -n "$ZELLIJ" || "${CORTEX_MULTIPLEXER:-}" == "zellij" ]] } +_zellij_available() { + command -v zellij >/dev/null 2>&1 +} + _zellij_session_exists() { local session="$1" zellij list-sessions 2>/dev/null | awk '{print $1}' | grep -Fxq "$session" @@ -56,6 +80,16 @@ _zellij_layout_for_command() { cat > "$layout_file" </dev/null; then - tmux attach -t "$session" - else - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "cd '$resolved' && claude --enable-auto-mode --dangerously-skip-permissions" Enter - tmux attach -t "$session" - fi + cd "$resolved" && claude --enable-auto-mode --dangerously-skip-permissions fi } -# Abrir OpenCode en tmux/cmux +# Abrir OpenCode en Zellij/cmux o en el directorio actual. # Mantiene el mismo patrón de uso que cc() pero usando opencode oc() { local target="${1:-.}" @@ -221,8 +266,11 @@ oc() { local oc_cmd="opencode ${OPENCODE_DEFAULT_FLAGS:-}" - if _zellij_preferred; then + if _zellij_preferred && _zellij_available; then _zellij_open_agent "$resolved" "$oc_cmd" + elif _zellij_preferred; then + echo "⚠️ zellij no está instalado; ejecutando OpenCode directo" + cd "$resolved" && eval "$oc_cmd" elif [[ -n "$CMUX_WORKSPACE_ID" ]]; then local workspace_name workspace_name="$(_workspace_name_for_path "$resolved")" @@ -254,15 +302,7 @@ oc() { _cmux_sidebar_refresh "$resolved" cd "$resolved" && eval "$oc_cmd" else - local session - session="$(_workspace_name_for_path "$resolved")" - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - else - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "cd '$resolved' && $oc_cmd" Enter - tmux attach -t "$session" - fi + cd "$resolved" && eval "$oc_cmd" fi } @@ -279,8 +319,11 @@ ccb() { local cc_cmd="claude --dangerously-skip-permissions" - if _zellij_preferred; then + if _zellij_preferred && _zellij_available; then _zellij_open_agent "$resolved" "$cc_cmd" + elif _zellij_preferred; then + echo "⚠️ zellij no está instalado; ejecutando Claude Code directo" + cd "$resolved" && eval "$cc_cmd" elif [[ -n "$CMUX_WORKSPACE_ID" ]]; then local workspace_name workspace_name="$(_workspace_name_for_path "$resolved")" @@ -312,15 +355,7 @@ ccb() { _cmux_sidebar_refresh "$resolved" cd "$resolved" && eval "$cc_cmd" else - local session - session="$(_workspace_name_for_path "$resolved")" - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - else - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "cd '$resolved' && $cc_cmd" Enter - tmux attach -t "$session" - fi + cd "$resolved" && eval "$cc_cmd" fi } @@ -337,8 +372,11 @@ ocb() { local oc_cmd="opencode ${OPENCODE_DEFAULT_FLAGS:-}" - if _zellij_preferred; then + if _zellij_preferred && _zellij_available; then _zellij_open_agent "$resolved" "$oc_cmd" + elif _zellij_preferred; then + echo "⚠️ zellij no está instalado; ejecutando OpenCode directo" + cd "$resolved" && eval "$oc_cmd" elif [[ -n "$CMUX_WORKSPACE_ID" ]]; then local workspace_name workspace_name="$(_workspace_name_for_path "$resolved")" @@ -370,15 +408,7 @@ ocb() { _cmux_sidebar_refresh "$resolved" cd "$resolved" && eval "$oc_cmd" else - local session - session="$(_workspace_name_for_path "$resolved")" - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - else - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "cd '$resolved' && $oc_cmd" Enter - tmux attach -t "$session" - fi + cd "$resolved" && eval "$oc_cmd" fi } @@ -400,7 +430,15 @@ ccx() { return 1 fi - if [[ -n "$CMUX_WORKSPACE_ID" ]]; then + if _zellij_preferred && _zellij_available; then + local ctxfile="$HOME/.claude/ccx-ctx-$$.txt" + echo "$context" > "$ctxfile" + chmod 600 "$ctxfile" + _zellij_open_agent "$resolved" "sh -c 'claude < $ctxfile; rm -f $ctxfile'" + elif _zellij_preferred; then + echo "⚠️ zellij no está instalado; ejecutando Claude Code directo" + cd "$resolved" && echo "$context" | claude + elif [[ -n "$CMUX_WORKSPACE_ID" ]]; then # Estamos dentro de cmux: escribir contexto a tempfile y abrir workspace propio local workspace_name workspace_name="$(_workspace_name_for_path "$resolved")" @@ -424,15 +462,7 @@ ccx() { elif [[ -n "$TMUX" ]]; then cd "$resolved" && echo "$context" | claude else - local session - session="$(_workspace_name_for_path "$resolved")" - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - else - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "cd '$resolved' && echo ${(q)context} | claude" Enter - tmux attach -t "$session" - fi + cd "$resolved" && echo "$context" | claude fi } diff --git a/zsh/scripts/tmux-helpers.zsh b/zsh/scripts/tmux-helpers.zsh index f8ecefc..a3cd4c0 100644 --- a/zsh/scripts/tmux-helpers.zsh +++ b/zsh/scripts/tmux-helpers.zsh @@ -1,29 +1,46 @@ -# tmux-helpers.zsh — Helpers para gestión de sesiones tmux +# tmux-helpers.zsh — Compat aliases sobre Zellij -# Alias base -alias t="tmux" +# Alias base: mantenemos la memoria muscular `t*`, pero el backend default es Zellij. +alias t="zellij" # Listar sesiones activas tl() { - tmux ls 2>/dev/null || echo "No hay sesiones tmux activas" + zellij list-sessions 2>/dev/null || echo "No hay sesiones Zellij activas" } # Attach a una sesión (o crearla si no existe) ta() { local session="${1:-main}" - tmux attach -t "$session" 2>/dev/null || tmux new-session -s "$session" + local layout_file + session="$(_zellij_context_label):$session" + layout_file="$(_zellij_default_layout)" + if _zellij_session_exists "$session"; then + zellij attach "$session" + elif [[ -n "$layout_file" ]]; then + zellij --session "$session" --layout "$layout_file" + else + zellij attach "$session" --create + fi } # Nueva sesión con nombre tn() { local session="${1:?Uso: tn }" - tmux new-session -s "$session" + local layout_file + session="$(_zellij_context_label):$session" + layout_file="$(_zellij_default_layout)" + if [[ -n "$layout_file" ]]; then + zellij --session "$session" --layout "$layout_file" + else + zellij attach "$session" --create + fi } # Matar una sesión tk() { local session="${1:?Uso: tk }" - tmux kill-session -t "$session" && echo "✓ Sesión '$session' terminada" + [[ "$session" == *:* ]] || session="$(_zellij_context_label):$session" + zellij delete-session "$session" --force && echo "✓ Sesión '$session' terminada" } # Sesión de desarrollo: nombre = basename del directorio actual @@ -34,18 +51,10 @@ tdev() { ta "$session" } -# Sesión de Claude Code en tmux +# Sesión de Claude Code en Zellij # Uso: cd ~/dev/work/myproject && tcc tcc() { local session session="$(basename "$PWD" | tr '.' '-')" - - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - return - fi - - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "claude" Enter - tmux attach -t "$session" + CORTEX_MULTIPLEXER=zellij cc "$PWD" } diff --git a/zsh/scripts/worktree-helpers.zsh b/zsh/scripts/worktree-helpers.zsh index 47ee32c..57d0e75 100644 --- a/zsh/scripts/worktree-helpers.zsh +++ b/zsh/scripts/worktree-helpers.zsh @@ -1,5 +1,5 @@ #region Worktree Helpers -# Funciones para gestión de git worktrees con integración cmux. +# Funciones para gestión de git worktrees con integración Zellij. # Detecta repos PCSoft automáticamente y bloquea la creación de worktrees en ellos. # Verifica si el repo actual contiene archivos PCSoft (Categoría B — prohibido worktree) @@ -32,7 +32,7 @@ _wt_path_for() { printf '%s/%s/%s' "$(_wt_base_dir)" "$repo_name" "$name" } -# Crea un worktree en ~/dev/worktrees// y abre workspace cmux automáticamente. +# Crea un worktree en ~/dev/worktrees// y abre sesión Zellij automáticamente. # Uso: wtadd [branch] # — nombre del worktree (crea ~/dev/worktrees//) # [branch] — branch existente o nueva (default: crea branch nueva con el mismo nombre) @@ -82,9 +82,9 @@ wtadd() { echo "✓ Worktree creado: $wt_path" - # Abrir workspace cmux si está disponible - if [[ -n "$CMUX_WORKSPACE_ID" ]] || command -v cmux &>/dev/null; then - echo "→ Abriendo workspace cmux..." + # Abrir el agente en el multiplexor default si está disponible. + if [[ "${CORTEX_MULTIPLEXER:-zellij}" == "zellij" ]] || [[ -n "$ZELLIJ" ]]; then + echo "→ Abriendo sesión Zellij..." cc "$wt_path" fi } diff --git a/zsh/zshrc b/zsh/zshrc index aa67a58..dd368e9 100644 --- a/zsh/zshrc +++ b/zsh/zshrc @@ -56,6 +56,7 @@ export VISUAL="$EDITOR" # Workspace principal de desarrollo export WORKSPACE_DIR="${WORKSPACE_DIR:-$HOME/dev}" export CLAUDE_CODE_EFFORT_LEVEL=high +export CORTEX_MULTIPLEXER="${CORTEX_MULTIPLEXER:-zellij}" # Proyectos de la organización export WORK_PROJECTS_DIR="${WORK_PROJECTS_DIR:-$HOME/dev/work}" @@ -301,23 +302,21 @@ help-profile() { echo " ${C}ccd [sub] ${R}Navegar al Claude workspace" echo " ${C}ccclip ${R}Copiar código al clipboard" - echo "\n${T}Tmux — sesiones${R}" - echo " ${C}tcc ${R}Abrir Claude Code + lazygit (layout automático)" + echo "\n${T}Zellij — sesiones${R}" + echo " ${C}zj [path] ${R}Entrar/crear sesión Zellij por repo/path" + echo " ${C}zsessions ${R}Listar sesiones Zellij" + echo " ${C}tcc ${R}Abrir Claude Code en sesión Zellij del repo" echo " ${C}tdev ${R}Sesión con nombre del directorio actual" echo " ${C}ta [nombre] ${R}Attach a sesión (o crearla)" echo " ${C}tn ${R}Nueva sesión con nombre" echo " ${C}tl ${R}Listar sesiones activas" echo " ${C}tk ${R}Matar sesión" - echo "\n${T}Tmux — keybindings${R}" - echo " ${C}Ctrl+A R ${R}Recargar config" - echo " ${C}Ctrl+A Space ${R}Ver todos los keybindings (which-key)" - echo " ${C}Ctrl+A | ${R}Split vertical" - echo " ${C}Ctrl+A - ${R}Split horizontal" - echo " ${C}Ctrl+A hjkl ${R}Navegar entre paneles" - echo " ${C}Ctrl+A Ctrl+S ${R}Guardar sesiones (resurrect)" - echo " ${C}Ctrl+A Ctrl+R ${R}Restaurar sesiones (resurrect)" - echo " ${C}Option+G ${R}Popup flotante (scratch)" + echo "\n${T}Zellij — keybindings${R}" + echo " ${C}Ctrl+P ${R}Modo pane" + echo " ${C}Ctrl+T ${R}Modo tab" + echo " ${C}Ctrl+S ${R}Modo resize" + echo " ${C}Ctrl+G ${R}Bloquear/desbloquear keybindings" echo "" } From d0cbb95c45341de289ba70833ab9b1f7ec3f1986 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Tue, 30 Jun 2026 01:13:49 +0000 Subject: [PATCH 4/6] feat: add remote zellij workflow --- README.md | 32 +++- ghostty/cmux.conf | 18 +- ghostty/config | 18 +- ghostty/muxy.conf | 18 +- install.sh | 321 ++++++++++++++++++--------------- starship/starship.toml | 18 ++ zellij/config.kdl | 43 +++++ zellij/layouts/cortex.kdl | 15 ++ zsh/scripts/claude-helpers.zsh | 96 ++++++++-- zsh/scripts/ssh-helpers.zsh | 112 ++++++++++++ zsh/zshrc | 13 ++ 11 files changed, 516 insertions(+), 188 deletions(-) create mode 100644 zellij/layouts/cortex.kdl create mode 100644 zsh/scripts/ssh-helpers.zsh diff --git a/README.md b/README.md index 0664feb..43c3588 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Configuración local extraida de `cortex`: terminal, shell, prompt, helpers de A - **Terminal**: [Ghostty](https://ghostty.org/) - **Shell**: Zsh nativo de macOS - **Prompt**: [Starship](https://starship.rs/) — tema Gruvbox Dark -- **Multiplexor**: [Zellij](https://zellij.dev/) + helpers de sesión +- **Multiplexor**: [Zellij](https://zellij.dev/) + helpers de sesión; tmux queda como compat/legacy - **Barra macOS**: [SketchyBar](https://github.com/FelixKratz/SketchyBar) con tema Gruvbox - **Window manager macOS**: [yabai](https://github.com/koekeishiya/yabai) + [skhd](https://github.com/koekeishiya/skhd) opcional y gradual - **Keyboard remaps macOS**: [Karabiner-Elements](https://karabiner-elements.pqrs.org/) con profile `cortex` @@ -29,7 +29,7 @@ bash install.sh ``` El instalador macOS: -1. Instala dependencias via Homebrew (starship, zellij, lazygit, micro, eza, sketchybar, yabai, skhd, Karabiner-Elements, FiraCode Nerd Font) +1. Instala dependencias via Homebrew (starship, tmux, zellij, mosh, lazygit, micro, eza, sketchybar, yabai, skhd, Karabiner-Elements, FiraCode Nerd Font) 2. Hace backup de configs existentes con timestamp 3. Crea symlinks de los dotfiles y guardrails globales (`.npmrc`, `pnpm/rc`, `.bunfig.toml`, `uv.toml`) 4. Intenta seleccionar el profile `cortex` de Karabiner si `karabiner_cli` está disponible @@ -53,6 +53,7 @@ dotfiles/ │ └── scripts/ │ ├── claude-helpers.zsh # Integración Claude Code │ ├── git-helpers.zsh # Identidades Git y clone helpers +│ ├── ssh-helpers.zsh # SSH/Mosh con contexto visible y sesiones Zellij remotas por repo │ ├── tmux-helpers.zsh # Compat aliases t* sobre Zellij │ ├── worktree-helpers.zsh # Helpers git worktree │ ├── screenshots.zsh # Manejo de screenshots macOS @@ -84,6 +85,11 @@ dotfiles/ | `oc [path]` | Abrir OpenCode | | `zj [path]` | Entrar/crear sesión Zellij por repo/path | | `zsessions` | Listar sesiones Zellij | +| `moshx [remote-path]` | Mosh al host; con path entra al Zellij remoto del repo | +| `moshx-doctor ` | Verifica `mosh-server`, `zellij`, `git` y `sh` en el host remoto | +| `sshx ` | SSH en sesión Zellij `ssh-` | +| `sshc ` | SSH directo con host visible en prompt | +| `whereami` | Mostrar host, cwd, repo, sesión y SSH | | `ccclip ` | Copiar código al clipboard | | `tcc`, `tdev`, `ta`, `tn`, `tl`, `tk` | Helpers Zellij compatibles con la memoria muscular tmux | | `wtadd`, `wtlist`, `wtremove` | Helpers de git worktrees | @@ -129,6 +135,28 @@ Comportamiento: - El layout `innit` muestra tab bar arriba y status bar abajo usando el theme `innit`. - Si necesitás evitar Zellij puntualmente, seteá `CORTEX_MULTIPLEXER` vacío en esa shell y ejecutá el agente directo. +La config versionada de Zellij prioriza orientación: + +- Barra superior con tabs y barra inferior de estado siempre visibles. +- Pane frames activados para ver límites y foco. +- Prompt Starship marca `zj:` cuando estás dentro de Zellij. +- `whereami` y `zwhere` muestran ubicación completa sin depender de la UI. + +Para workstation remota persistente, la unidad principal es una sesión Zellij por repo en el host remoto. Mosh es sólo el transporte resiliente: + +```bash +moshx agent-dev-01 ~/dev/personal/cortex +moshx agent-dev-01 ~/dev/personal/infra +``` + +Cada comando entra por Mosh y hace attach/create de una sesión Zellij remota nombrada por el repo. Para hosts sin Mosh o troubleshooting, `sshx agent-dev-01` queda como fallback SSH clásico. + +Si falla, diagnosticá primero: + +```bash +moshx-doctor agent-dev-01 +``` + ## SketchyBar La config macOS enlaza `sketchybar/` en `~/.config/sketchybar`. El diseño es sobrio, notch-safe y usa la paleta dark/green de InnIT. diff --git a/ghostty/cmux.conf b/ghostty/cmux.conf index 440013b..aedabe6 100644 --- a/ghostty/cmux.conf +++ b/ghostty/cmux.conf @@ -32,27 +32,27 @@ palette = 15=#ffffff # Transparencia background-opacity = 1.0 -unfocused-split-opacity = 0.2 +unfocused-split-opacity = 0.85 # Ventana -window-decoration = false +window-decoration = true window-padding-balance = true window-step-resize = false -macos-titlebar-style = hidden -window-padding-x = 8 -window-padding-y = 8 +macos-titlebar-style = tabs +window-padding-x = 4 +window-padding-y = 2 # Cursor cursor-style = block_hollow -# Cursor con efecto suave -# Opciones disponibles: cursor_smear_soft, cursor_blaze, cursor_frozen, cursor_smear_rainbow -custom-shader = shaders/cursor_smear_soft.glsl +# Shaders desactivados en modo trabajo remoto: reducen legibilidad en multiplexers. +# custom-shader = shaders/cursor_smear_soft.glsl # Integración con shell shell-integration = zsh -# Sin macos-option-as-alt: Option sigue siendo Option en ambos lados (tildes/ñ funcionan normal) +# Option izquierdo como Alt para Zellij/tmux; Option derecho conserva tildes/ñ. +macos-option-as-alt = left # Comportamiento scrollback-limit = 10000 diff --git a/ghostty/config b/ghostty/config index 85f23c9..3740eb4 100644 --- a/ghostty/config +++ b/ghostty/config @@ -33,27 +33,27 @@ palette = 15=#ffffff # Transparencia background-opacity = 1.0 -unfocused-split-opacity = 0.2 +unfocused-split-opacity = 0.85 # Ventana -window-decoration = false +window-decoration = true window-padding-balance = true window-step-resize = false -macos-titlebar-style = hidden -window-padding-x = 8 -window-padding-y = 8 +macos-titlebar-style = tabs +window-padding-x = 4 +window-padding-y = 2 # Cursor cursor-style = block_hollow -# Cursor con efecto suave -# Opciones disponibles: cursor_smear_soft, cursor_blaze, cursor_frozen, cursor_smear_rainbow -custom-shader = shaders/cursor_smear_soft.glsl +# Shaders desactivados en modo trabajo remoto: reducen legibilidad en multiplexers. +# custom-shader = shaders/cursor_smear_soft.glsl # Integración con shell shell-integration = zsh -# Sin macos-option-as-alt: Option sigue siendo Option en ambos lados (tildes/ñ funcionan normal) +# Option izquierdo como Alt para Zellij/tmux; Option derecho conserva tildes/ñ. +macos-option-as-alt = left # Comportamiento scrollback-limit = 10000 diff --git a/ghostty/muxy.conf b/ghostty/muxy.conf index f5b1373..c0bbb68 100644 --- a/ghostty/muxy.conf +++ b/ghostty/muxy.conf @@ -5,27 +5,27 @@ font-size = 14 # Transparencia background-opacity = 1.0 -unfocused-split-opacity = 0.2 +unfocused-split-opacity = 0.85 # Ventana -window-decoration = false +window-decoration = true window-padding-balance = true window-step-resize = false -macos-titlebar-style = hidden -window-padding-x = 8 -window-padding-y = 8 +macos-titlebar-style = tabs +window-padding-x = 4 +window-padding-y = 2 # Cursor cursor-style = block_hollow -# Cursor con efecto suave -# Opciones disponibles: cursor_smear_soft, cursor_blaze, cursor_frozen, cursor_smear_rainbow -custom-shader = shaders/cursor_smear_soft.glsl +# Shaders desactivados en modo trabajo remoto: reducen legibilidad en multiplexers. +# custom-shader = shaders/cursor_smear_soft.glsl # Integración con shell shell-integration = zsh -# Sin macos-option-as-alt: Option sigue siendo Option en ambos lados (tildes/ñ funcionan normal) +# Option izquierdo como Alt para Zellij/tmux; Option derecho conserva tildes/ñ. +macos-option-as-alt = left # Comportamiento scrollback-limit = 10000 diff --git a/install.sh b/install.sh index 55dc5e7..94a7fed 100755 --- a/install.sh +++ b/install.sh @@ -1,29 +1,53 @@ #!/bin/bash -# install.sh — Instalador de dotfiles macOS +# install.sh — Instalador de dotfiles macOS/Linux set -e DOTFILES="$(cd "$(dirname "$0")" && pwd)" TIMESTAMP=$(date +%Y%m%d_%H%M%S) +OS="$(uname -s)" +IS_MACOS=false +IS_LINUX=false + +case "$OS" in + Darwin) IS_MACOS=true ;; + Linux) IS_LINUX=true ;; + *) + echo "❌ Sistema no soportado: $OS" + exit 1 + ;; +esac echo "" echo " ╔══════════════════════════════════════╗" -echo " ║ dotfiles — Instalador macOS ║" +echo " ║ dotfiles — Instalador macOS/Linux ║" echo " ╚══════════════════════════════════════╝" echo "" -# --- Verificar dependencias base --- -if ! command -v brew &>/dev/null; then - echo "❌ Homebrew no está instalado. Instalá desde https://brew.sh" - exit 1 -fi +# --- Verificar herramientas --- +echo "📦 Verificando herramientas ($OS)..." -if ! command -v starship &>/dev/null; then - echo "📦 Instalando starship..." - brew install starship -fi +install_with_brew() { + local command_name="$1" + local package_name="$2" + local description="$3" + + if command -v "$command_name" &>/dev/null; then + echo " ✓ $command_name ya instalado" + return + fi -# --- Instalar herramientas opcionales --- -echo "📦 Verificando herramientas..." + if $IS_MACOS; then + if ! command -v brew &>/dev/null; then + echo "❌ Homebrew no está instalado. Instalá desde https://brew.sh" + exit 1 + fi + + echo " → Instalando $description..." + brew install "$package_name" + else + echo " ! $command_name no está instalado; instalalo con el package manager de esta distro si lo necesitás" + fi +} karabiner_app_exists() { [[ -d "/Applications/Karabiner-Elements.app" || -d "$HOME/Applications/Karabiner-Elements.app" ]] @@ -41,80 +65,61 @@ karabiner_cli_path() { fi } -if ! command -v micro &>/dev/null; then - echo " → Instalando micro (editor terminal)..." - brew install micro -else - echo " ✓ micro ya instalado" -fi - -if ! command -v eza &>/dev/null; then - echo " → Instalando eza (ls mejorado)..." - brew install eza -else - echo " ✓ eza ya instalado" -fi - -if ! command -v zellij &>/dev/null; then - echo " → Instalando zellij (multiplexor de terminal)..." - brew install zellij -else - echo " ✓ zellij ya instalado" -fi - -if ! command -v lazygit &>/dev/null; then - echo " → Instalando lazygit (git TUI)..." - brew install lazygit -else - echo " ✓ lazygit ya instalado" -fi - -if ! command -v sketchybar &>/dev/null; then - echo " → Instalando sketchybar (barra macOS)..." - brew install sketchybar -else - echo " ✓ sketchybar ya instalado" -fi - -if ! command -v yabai &>/dev/null; then - echo " → Instalando yabai (window manager macOS)..." - brew tap koekeishiya/formulae - brew install yabai -else - echo " ✓ yabai ya instalado" -fi +install_with_brew starship starship "starship" +install_with_brew micro micro "micro (editor terminal)" +install_with_brew eza eza "eza (ls mejorado)" +install_with_brew tmux tmux "tmux (multiplexor de terminal)" +install_with_brew zellij zellij "zellij (multiplexor remoto persistente)" +install_with_brew mosh mosh "mosh (SSH resiliente para workstations remotas)" +install_with_brew lazygit lazygit "lazygit (git TUI)" + +if $IS_MACOS; then + install_with_brew sketchybar sketchybar "sketchybar (barra macOS)" + + if ! command -v yabai &>/dev/null; then + echo " → Instalando yabai (window manager macOS)..." + brew tap koekeishiya/formulae + brew install yabai + else + echo " ✓ yabai ya instalado" + fi -if ! command -v skhd &>/dev/null; then - echo " → Instalando skhd (hotkeys macOS)..." - brew tap koekeishiya/formulae - brew install skhd -else - echo " ✓ skhd ya instalado" -fi + if ! command -v skhd &>/dev/null; then + echo " → Instalando skhd (hotkeys macOS)..." + brew tap koekeishiya/formulae + brew install skhd + else + echo " ✓ skhd ya instalado" + fi -if ! karabiner_cli_available && ! karabiner_app_exists; then - echo " → Instalando Karabiner-Elements..." - brew install --cask karabiner-elements -else - echo " ✓ Karabiner-Elements ya instalado" + if ! karabiner_cli_available && ! karabiner_app_exists; then + echo " → Instalando Karabiner-Elements..." + brew install --cask karabiner-elements + else + echo " ✓ Karabiner-Elements ya instalado" + fi fi # --- Fuentes --- echo "" echo "📦 Verificando fuentes..." -if ! ls "$HOME/Library/Fonts/FiraCodeNerdFont"* &>/dev/null 2>&1; then - echo " → Instalando FiraCode Nerd Font..." - brew install --cask font-fira-code-nerd-font - echo " ✓ FiraCode Nerd Font instalada" +if $IS_MACOS; then + if ! ls "$HOME/Library/Fonts/FiraCodeNerdFont"* &>/dev/null 2>&1; then + echo " → Instalando FiraCode Nerd Font..." + brew install --cask font-fira-code-nerd-font + echo " ✓ FiraCode Nerd Font instalada" + else + echo " ✓ FiraCode Nerd Font ya instalada" + fi + + mkdir -p "$HOME/Library/Fonts" + cp "$DOTFILES/fonts/FiraCodeNerdFontMonoBeard-Reg.ttf" "$HOME/Library/Fonts/FiraCodeNerdFontMonoBeard-Reg.ttf" + echo " ✓ FiraCode Nerd Font Mono Beard instalada" else - echo " ✓ FiraCode Nerd Font ya instalada" + echo " - Fuentes macOS omitidas en Linux" fi -mkdir -p "$HOME/Library/Fonts" -cp "$DOTFILES/fonts/FiraCodeNerdFontMonoBeard-Reg.ttf" "$HOME/Library/Fonts/FiraCodeNerdFontMonoBeard-Reg.ttf" -echo " ✓ FiraCode Nerd Font Mono Beard instalada" - # --- Backup de configs existentes --- echo "" echo "💾 Haciendo backup de configs existentes..." @@ -143,21 +148,29 @@ backup_karabiner_if_exists() { backup_if_exists "$HOME/.zshrc" backup_if_exists "$HOME/.npmrc" -backup_if_exists "$HOME/Library/Preferences/pnpm/rc" backup_if_exists "$HOME/.bunfig.toml" backup_if_exists "$HOME/.config/uv/uv.toml" backup_if_exists "$HOME/.config/starship.toml" -backup_if_exists "$HOME/.config/ghostty/config" backup_if_exists "$HOME/.config/zellij/config.kdl" backup_if_exists "$HOME/.config/zellij/layouts/innit.kdl" +backup_if_exists "$HOME/.config/zellij/layouts/cortex.kdl" +backup_if_exists "$HOME/.tmux.conf" backup_if_exists "$HOME/.claude/statusline.sh" backup_if_exists "$HOME/.config/lazygit/config.yml" -backup_if_exists "$HOME/.config/sketchybar" -backup_if_exists "$HOME/.config/yabai/yabairc" -backup_if_exists "$HOME/.yabairc" -backup_if_exists "$HOME/.config/skhd/skhdrc" -backup_if_exists "$HOME/.skhdrc" -backup_karabiner_if_exists + +if $IS_MACOS; then + backup_if_exists "$HOME/Library/Preferences/pnpm/rc" + backup_if_exists "$HOME/.config/ghostty/config" + backup_if_exists "$HOME/Library/Application Support/com.cmuxterm.app/config.ghostty" + backup_if_exists "$HOME/.config/sketchybar" + backup_if_exists "$HOME/.config/yabai/yabairc" + backup_if_exists "$HOME/.yabairc" + backup_if_exists "$HOME/.config/skhd/skhdrc" + backup_if_exists "$HOME/.skhdrc" + backup_karabiner_if_exists +else + backup_if_exists "$HOME/.config/pnpm/rc" +fi # --- Crear symlinks --- echo "" @@ -175,87 +188,100 @@ create_symlink() { create_symlink "$DOTFILES/zsh/zshrc" "$HOME/.zshrc" create_symlink "$DOTFILES/npm/npmrc" "$HOME/.npmrc" -create_symlink "$DOTFILES/pnpm/rc" "$HOME/Library/Preferences/pnpm/rc" create_symlink "$DOTFILES/bun/bunfig.toml" "$HOME/.bunfig.toml" create_symlink "$DOTFILES/uv/uv.toml" "$HOME/.config/uv/uv.toml" create_symlink "$DOTFILES/starship/starship.toml" "$HOME/.config/starship.toml" -create_symlink "$DOTFILES/ghostty/config" "$HOME/.config/ghostty/config" -create_symlink "$DOTFILES/ghostty/shaders" "$HOME/.config/ghostty/shaders" create_symlink "$DOTFILES/zellij/config.kdl" "$HOME/.config/zellij/config.kdl" create_symlink "$DOTFILES/zellij/layouts/innit.kdl" "$HOME/.config/zellij/layouts/innit.kdl" +create_symlink "$DOTFILES/zellij/layouts/cortex.kdl" "$HOME/.config/zellij/layouts/cortex.kdl" +create_symlink "$DOTFILES/tmux/tmux.conf" "$HOME/.tmux.conf" chmod +x "$DOTFILES/claude/statusline.sh" create_symlink "$DOTFILES/claude/statusline.sh" "$HOME/.claude/statusline.sh" create_symlink "$DOTFILES/micro/settings.json" "$HOME/.config/micro/settings.json" create_symlink "$DOTFILES/lazygit/config.yml" "$HOME/.config/lazygit/config.yml" -chmod +x "$DOTFILES/sketchybar/sketchybarrc" "$DOTFILES/sketchybar/plugins"/*.sh -create_symlink "$DOTFILES/sketchybar" "$HOME/.config/sketchybar" -chmod +x "$DOTFILES/yabai/yabairc" -create_symlink "$DOTFILES/yabai/yabairc" "$HOME/.config/yabai/yabairc" -create_symlink "$DOTFILES/yabai/yabairc" "$HOME/.yabairc" -create_symlink "$DOTFILES/skhd/skhdrc" "$HOME/.config/skhd/skhdrc" -create_symlink "$DOTFILES/skhd/skhdrc" "$HOME/.skhdrc" -create_symlink "$DOTFILES/karabiner/karabiner.json" "$HOME/.config/karabiner/karabiner.json" - -KARABINER_CLI="$(karabiner_cli_path || true)" -if [[ -n "$KARABINER_CLI" ]]; then - if "$KARABINER_CLI" --select-profile cortex &>/dev/null; then - echo " ✓ Perfil Karabiner cortex seleccionado" - else - echo " ! No se pudo seleccionar el perfil Karabiner cortex; abrí Karabiner-Elements para activarlo" + +if $IS_MACOS; then + create_symlink "$DOTFILES/pnpm/rc" "$HOME/Library/Preferences/pnpm/rc" + create_symlink "$DOTFILES/ghostty/config" "$HOME/.config/ghostty/config" + create_symlink "$DOTFILES/ghostty/cmux.conf" "$HOME/Library/Application Support/com.cmuxterm.app/config.ghostty" + create_symlink "$DOTFILES/ghostty/shaders" "$HOME/.config/ghostty/shaders" + chmod +x "$DOTFILES/sketchybar/sketchybarrc" "$DOTFILES/sketchybar/plugins"/*.sh + create_symlink "$DOTFILES/sketchybar" "$HOME/.config/sketchybar" + chmod +x "$DOTFILES/yabai/yabairc" + create_symlink "$DOTFILES/yabai/yabairc" "$HOME/.config/yabai/yabairc" + create_symlink "$DOTFILES/yabai/yabairc" "$HOME/.yabairc" + create_symlink "$DOTFILES/skhd/skhdrc" "$HOME/.config/skhd/skhdrc" + create_symlink "$DOTFILES/skhd/skhdrc" "$HOME/.skhdrc" + create_symlink "$DOTFILES/karabiner/karabiner.json" "$HOME/.config/karabiner/karabiner.json" + + KARABINER_CLI="$(karabiner_cli_path || true)" + if [[ -n "$KARABINER_CLI" ]]; then + if "$KARABINER_CLI" --select-profile cortex &>/dev/null; then + echo " ✓ Perfil Karabiner cortex seleccionado" + else + echo " ! No se pudo seleccionar el perfil Karabiner cortex; abrí Karabiner-Elements para activarlo" + fi fi +else + create_symlink "$DOTFILES/pnpm/rc" "$HOME/.config/pnpm/rc" fi -# --- Servicios macOS --- -echo "" -echo "🚀 Asegurando servicios macOS..." - -warn_service() { - local name="$1" - local command_hint="$2" - echo " ! No se pudo iniciar $name. macOS puede requerir permisos en Privacy & Security > Accessibility." - echo " Fallback manual: $command_hint" -} - -if command -v brew &>/dev/null && command -v sketchybar &>/dev/null; then - if brew services start sketchybar &>/dev/null; then - echo " ✓ sketchybar iniciado via brew services" +if $IS_MACOS; then + # --- Servicios macOS --- + echo "" + echo "🚀 Asegurando servicios macOS..." + + warn_service() { + local name="$1" + local command_hint="$2" + echo " ! No se pudo iniciar $name. macOS puede requerir permisos en Privacy & Security > Accessibility." + echo " Fallback manual: $command_hint" + } + + if command -v brew &>/dev/null && command -v sketchybar &>/dev/null; then + if brew services start sketchybar &>/dev/null; then + echo " ✓ sketchybar iniciado via brew services" + else + warn_service "sketchybar" "brew services start sketchybar" + fi else - warn_service "sketchybar" "brew services start sketchybar" + echo " - sketchybar no disponible; se omite" fi -else - echo " - sketchybar no disponible; se omite" -fi -if command -v yabai &>/dev/null; then - if pgrep -x yabai &>/dev/null; then - if yabai --restart-service &>/dev/null; then - echo " ✓ yabai reiniciado" + if command -v yabai &>/dev/null; then + if pgrep -x yabai &>/dev/null; then + if yabai --restart-service &>/dev/null; then + echo " ✓ yabai reiniciado" + else + warn_service "yabai" "yabai --restart-service" + fi + elif yabai --start-service &>/dev/null; then + echo " ✓ yabai iniciado" else - warn_service "yabai" "yabai --restart-service" + warn_service "yabai" "yabai --start-service" fi - elif yabai --start-service &>/dev/null; then - echo " ✓ yabai iniciado" else - warn_service "yabai" "yabai --start-service" + echo " - yabai no disponible; se omite" fi -else - echo " - yabai no disponible; se omite" -fi -if command -v skhd &>/dev/null; then - if pgrep -x skhd &>/dev/null; then - if skhd --reload &>/dev/null; then - echo " ✓ skhd recargado" + if command -v skhd &>/dev/null; then + if pgrep -x skhd &>/dev/null; then + if skhd --reload &>/dev/null; then + echo " ✓ skhd recargado" + else + warn_service "skhd" "skhd --reload" + fi + elif skhd --start-service &>/dev/null; then + echo " ✓ skhd iniciado" else - warn_service "skhd" "skhd --reload" + warn_service "skhd" "skhd --start-service" fi - elif skhd --start-service &>/dev/null; then - echo " ✓ skhd iniciado" else - warn_service "skhd" "skhd --start-service" + echo " - skhd no disponible; se omite" fi else - echo " - skhd no disponible; se omite" + echo "" + echo "🚀 Servicios macOS omitidos en Linux" fi # --- Copiar colorschemes de micro (no pueden ser symlink) --- @@ -279,12 +305,17 @@ echo "" echo "✅ Instalación completada!" echo "" echo " Próximos pasos:" -echo " 1. Abrí una nueva tab en Ghostty para cargar el nuevo profile" +echo " 1. Abrí una nueva shell para cargar el nuevo profile" echo " 2. Editá local/env.zsh con tus paths personales" -echo " 3. Ghostty ya usa FiraCode Nerd Font Mono Beard (reiniciá si no se ve bien)" -echo " 4. Usá zj, cc u oc para abrir sesiones Zellij por repo" -echo " 5. Si macOS bloqueó servicios, habilitá Accessibility y corré los fallbacks impresos arriba" -echo " 6. Abrí Karabiner-Elements y habilitá Input Monitoring/Accessibility si macOS lo pide" +if $IS_MACOS; then + echo " 3. Ghostty ya usa FiraCode Nerd Font Mono Beard (reiniciá si no se ve bien)" + echo " 4. Usá zj, cc u oc para abrir sesiones Zellij por repo" + echo " 5. Si macOS bloqueó servicios, habilitá Accessibility y corré los fallbacks impresos arriba" + echo " 6. Abrí Karabiner-Elements y habilitá Input Monitoring/Accessibility si macOS lo pide" +else + echo " 3. Instalá manualmente herramientas faltantes que el script haya marcado con !" + echo " 4. Usá zj, cc u oc para abrir sesiones Zellij por repo" +fi echo "" echo " Para medir el load time:" echo " \$ time zsh -i -c exit" diff --git a/starship/starship.toml b/starship/starship.toml index e975bef..09578d7 100644 --- a/starship/starship.toml +++ b/starship/starship.toml @@ -1,5 +1,7 @@ # Formato general format = """ +${custom.ssh_context}\ +${custom.zellij_context}\ ${custom.repo}\ [|](fg:color_orange)\ $directory\ @@ -90,6 +92,22 @@ command = "basename \"$(git rev-parse --show-toplevel 2>/dev/null)\" | cut -c1-2 style = "fg:color_fg0 bg:color_orange" format = '[ $output ]($style)' +[custom.ssh_context] +description = "Host visible en shells remotos" +when = "test -n \"$SSH_CONNECTION\"" +shell = ["bash", "--noprofile", "--norc"] +command = "printf 'ssh:%s' \"${CORTEX_SSH_TARGET:-$(hostname -s 2>/dev/null || hostname)}\" | cut -c1-28" +style = "fg:color_fg0 bg:color_red" +format = '[ $output ]($style)[|](fg:color_red)' + +[custom.zellij_context] +description = "Sesión Zellij visible en el prompt" +when = "test -n \"$ZELLIJ\"" +shell = ["bash", "--noprofile", "--norc"] +command = "printf 'zj:%s' \"${ZELLIJ_SESSION_NAME:-?}\" | cut -c1-28" +style = "fg:color_fg0 bg:color_purple" +format = '[ $output ]($style)[|](fg:color_purple)' + [custom.sdd_auto] description = "Estado SDD auto del proyecto" when = "git rev-parse --is-inside-work-tree >/dev/null 2>&1 && test -f .ai/sdd-session.json && (jq -r '.execution_mode // empty' .ai/sdd-session.json 2>/dev/null || python3 -c \"import json; print(json.load(open('.ai/sdd-session.json')).get('execution_mode',''))\" 2>/dev/null) | grep -qx 'auto'" diff --git a/zellij/config.kdl b/zellij/config.kdl index f2600ce..3651905 100644 --- a/zellij/config.kdl +++ b/zellij/config.kdl @@ -1,5 +1,24 @@ theme "innit" +// Zellij config — orientation-first UX for local and remote dev. +// Goal: always know session, tab, pane, host and path at a glance. + +simplified_ui true +pane_frames true +default_mode "normal" +mouse_mode true +copy_on_select true +scroll_buffer_size 20000 +session_serialization true +pane_viewport_serialization true +scrollback_lines_to_serialize 5000 + +ui { + pane_frames { + rounded_corners true + } +} + themes { innit { fg 248 250 252 @@ -33,3 +52,27 @@ themes { } } } + +keybinds { + normal { + bind "Alt h" "Alt Left" { MoveFocusOrTab "Left"; } + bind "Alt j" "Alt Down" { MoveFocus "Down"; } + bind "Alt k" "Alt Up" { MoveFocus "Up"; } + bind "Alt l" "Alt Right" { MoveFocusOrTab "Right"; } + bind "Alt n" { NewPane; } + bind "Alt |" { NewPane "Right"; } + bind "Alt -" { NewPane "Down"; } + bind "Alt f" { ToggleFocusFullscreen; } + bind "Alt s" { SwitchToMode "session"; } + bind "Alt r" { RenameSession; } + bind "Alt t" { NewTab; } + bind "Alt 1" { GoToTab 1; } + bind "Alt 2" { GoToTab 2; } + bind "Alt 3" { GoToTab 3; } + bind "Alt 4" { GoToTab 4; } + bind "Alt 5" { GoToTab 5; } + bind "Alt ?" { SwitchToMode "tmux"; } + } +} + +default_layout "innit" diff --git a/zellij/layouts/cortex.kdl b/zellij/layouts/cortex.kdl new file mode 100644 index 0000000..e02e015 --- /dev/null +++ b/zellij/layouts/cortex.kdl @@ -0,0 +1,15 @@ +layout { + default_tab_template { + pane size=1 borderless=true { + plugin location="zellij:tab-bar" + } + children + pane size=2 borderless=true { + plugin location="zellij:status-bar" + } + } + + tab name="shell" focus=true { + pane + } +} diff --git a/zsh/scripts/claude-helpers.zsh b/zsh/scripts/claude-helpers.zsh index 86c25ad..8ee8dda 100644 --- a/zsh/scripts/claude-helpers.zsh +++ b/zsh/scripts/claude-helpers.zsh @@ -57,7 +57,19 @@ _zellij_available() { _zellij_session_exists() { local session="$1" - zellij list-sessions 2>/dev/null | awk '{print $1}' | grep -Fxq "$session" + zellij list-sessions --short --no-formatting 2>/dev/null | grep -Fxq "$session" +} + +_zellij_session_exited() { + local session="$1" + zellij list-sessions --no-formatting 2>/dev/null | grep -Eq "^${session}[[:space:]].*EXITED" +} + +_zellij_recreate_if_exited() { + local session="$1" + if _zellij_session_exited "$session"; then + zellij delete-session "$session" >/dev/null 2>&1 || true + fi } _zellij_kdl_escape() { @@ -67,16 +79,31 @@ _zellij_kdl_escape() { printf '%s' "$value" } +_zellij_agent_session_name() { + local resolved="$1" + local agent="$2" + local base + base="$(_workspace_name_for_path "$resolved")" + + if [[ -n "$agent" ]]; then + printf '%s-%s' "$base" "$agent" + else + printf '%s' "$base" + fi +} + _zellij_layout_for_command() { local resolved="$1" local command_line="$2" + local title="${3:-$(basename "$resolved")}" local layout_file layout_file=$(mktemp "${TMPDIR:-/tmp}/cortex-zellij-layout.XXXXXX.kdl") - local cwd_escaped command_escaped shell_name + local cwd_escaped command_escaped shell_name title_escaped cwd_escaped="$(_zellij_kdl_escape "$resolved")" command_escaped="$(_zellij_kdl_escape "cd ${(q)resolved} && $command_line; exec ${SHELL:-zsh}")" shell_name="$(_zellij_kdl_escape "${SHELL:-zsh}")" + title_escaped="$(_zellij_kdl_escape "$title")" cat > "$layout_file" </dev/null 2>&1; then + echo "Repo: $(git rev-parse --show-toplevel)" + echo "Branch: $(git branch --show-current 2>/dev/null || printf detached)" + fi +} + +zn() { + local target="${1:-.}" + local agent="${2:-}" + local resolved + resolved=$(cd "$target" 2>/dev/null && pwd) + + if [[ -z "$resolved" ]]; then + echo "❌ Directorio no encontrado: $target" + return 1 + fi + + if [[ -z "$ZELLIJ" ]]; then + echo "❌ zn solo funciona dentro de Zellij" + return 1 + fi + + zellij action rename-session "$(_zellij_agent_session_name "$resolved" "$agent")" } _cmux_rename_workspace() { @@ -208,7 +276,7 @@ cc() { fi if _zellij_preferred && _zellij_available; then - _zellij_open_agent "$resolved" "claude --enable-auto-mode --dangerously-skip-permissions" + _zellij_open_agent "$resolved" "claude --enable-auto-mode --dangerously-skip-permissions" "claude" elif _zellij_preferred; then echo "⚠️ zellij no está instalado; ejecutando Claude Code directo" cd "$resolved" && claude --enable-auto-mode --dangerously-skip-permissions @@ -267,7 +335,7 @@ oc() { local oc_cmd="opencode ${OPENCODE_DEFAULT_FLAGS:-}" if _zellij_preferred && _zellij_available; then - _zellij_open_agent "$resolved" "$oc_cmd" + _zellij_open_agent "$resolved" "$oc_cmd" "opencode" elif _zellij_preferred; then echo "⚠️ zellij no está instalado; ejecutando OpenCode directo" cd "$resolved" && eval "$oc_cmd" @@ -320,7 +388,7 @@ ccb() { local cc_cmd="claude --dangerously-skip-permissions" if _zellij_preferred && _zellij_available; then - _zellij_open_agent "$resolved" "$cc_cmd" + _zellij_open_agent "$resolved" "$cc_cmd" "claude" elif _zellij_preferred; then echo "⚠️ zellij no está instalado; ejecutando Claude Code directo" cd "$resolved" && eval "$cc_cmd" @@ -373,7 +441,7 @@ ocb() { local oc_cmd="opencode ${OPENCODE_DEFAULT_FLAGS:-}" if _zellij_preferred && _zellij_available; then - _zellij_open_agent "$resolved" "$oc_cmd" + _zellij_open_agent "$resolved" "$oc_cmd" "opencode" elif _zellij_preferred; then echo "⚠️ zellij no está instalado; ejecutando OpenCode directo" cd "$resolved" && eval "$oc_cmd" diff --git a/zsh/scripts/ssh-helpers.zsh b/zsh/scripts/ssh-helpers.zsh new file mode 100644 index 0000000..484435c --- /dev/null +++ b/zsh/scripts/ssh-helpers.zsh @@ -0,0 +1,112 @@ +# ssh-helpers.zsh — Remote helpers with visible host/repo context. + +_remote_zellij_command_for_path() { + local remote_path="$1" + local shell_path + + if [[ "$remote_path" == "$HOME/"* ]]; then + shell_path="~/${remote_path#$HOME/}" + else + shell_path="${(q)remote_path}" + fi + + printf 'cd %s && root=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && repo=$(basename "$root" | tr . -) && parent=$(basename "$(dirname "$root")" | tr . -) && session="$parent-$repo" && zellij attach --create "$session"' "$shell_path" +} + +# Entrar a una workstation remota por Mosh. Si pasás path, attach/crea Zellij remoto por repo. +moshx() { + local target="${1:?Uso: moshx [remote-path]}" + shift + + if ! command -v mosh >/dev/null 2>&1; then + echo "❌ mosh no está instalado localmente; fallback: sshx $target ${*}" + sshx "$target" "$@" + return + fi + + local remote_path="${1:-}" + if [[ -z "$remote_path" ]]; then + CORTEX_SSH_TARGET="$target" mosh "$target" + return + fi + shift + + local remote_command + remote_command="$(_remote_zellij_command_for_path "$remote_path")" + CORTEX_SSH_TARGET="$target" mosh "$target" -- sh -lc "$remote_command" || { + echo "⚠️ mosh falló; probando fallback SSH al mismo Zellij remoto" + CORTEX_SSH_TARGET="$target" ssh -t "$target" sh -lc "$remote_command" + } +} + +# Diagnosticar dependencias remotas para moshx sin abrir sesión interactiva. +moshx-doctor() { + local target="${1:?Uso: moshx-doctor }" + + echo "Local:" + command -v mosh >/dev/null 2>&1 && echo " ✓ mosh: $(command -v mosh)" || echo " ✗ mosh local no encontrado" + + echo "Remote $target:" + ssh "$target" 'for cmd in mosh-server zellij git sh; do if command -v "$cmd" >/dev/null 2>&1; then printf " ✓ %s: %s\n" "$cmd" "$(command -v "$cmd")"; else printf " ✗ %s no encontrado\n" "$cmd"; fi; done' +} + +# SSH directo, pero exportando CORTEX_SSH_TARGET para que el prompt muestre el host remoto. +sshc() { + local target="${1:?Uso: sshc [ssh args...]}" + shift + CORTEX_SSH_TARGET="$target" ssh "$target" "$@" +} + +# Abrir SSH en una sesión Zellij local nombrada ssh-. Fallback para hosts sin Mosh. +sshx() { + local target="${1:?Uso: sshx [ssh args...]}" + shift + + if ! command -v zellij >/dev/null 2>&1; then + sshc "$target" "$@" + return + fi + + local session + session="ssh-${target//[^A-Za-z0-9_.-]/-}" + local quoted_args=("${(@q)@}") + local command_line="CORTEX_SSH_TARGET=${(q)target} ssh ${(q)target} ${quoted_args[*]}" + + if ! typeset -f _zellij_layout_for_command >/dev/null 2>&1 || ! typeset -f _zellij_session_exists >/dev/null 2>&1; then + CORTEX_SSH_TARGET="$target" zellij --session "$session" + return + fi + + if [[ -n "$ZELLIJ" ]]; then + if _zellij_session_exists "$session"; then + zellij action switch-session "$session" + return + fi + + local layout_file + layout_file="$(_zellij_layout_for_command "$PWD" "$command_line" "$session")" + zellij action switch-session --layout "$layout_file" "$session" + else + if _zellij_session_exists "$session"; then + zellij attach "$session" + return + fi + + local layout_file + layout_file="$(_zellij_layout_for_command "$PWD" "$command_line" "$session")" + zellij --session "$session" --layout "$layout_file" + fi +} + +# Mostrar contexto rápido del shell actual: host, cwd, git, zellij/tmux y ssh. +whereami() { + printf 'host: %s\n' "$(hostname -s 2>/dev/null || hostname)" + printf 'cwd: %s\n' "$PWD" + if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + printf 'repo: %s\n' "$(git rev-parse --show-toplevel)" + printf 'branch: %s\n' "$(git branch --show-current 2>/dev/null || printf detached)" + fi + [[ -n "$ZELLIJ" ]] && printf 'zellij: %s\n' "${ZELLIJ_SESSION_NAME:-unknown}" + [[ -n "$TMUX" ]] && printf 'tmux: %s\n' "$(tmux display-message -p '#S:#W.#P' 2>/dev/null || printf unknown)" + [[ -n "$SSH_CONNECTION" ]] && printf 'ssh: %s\n' "${CORTEX_SSH_TARGET:-remote}" +} diff --git a/zsh/zshrc b/zsh/zshrc index dd368e9..b20026a 100644 --- a/zsh/zshrc +++ b/zsh/zshrc @@ -220,6 +220,7 @@ if [[ -d "$_SCRIPTS_DIR" ]]; then [[ -f "$_SCRIPTS_DIR/screenshots.zsh" ]] && source "$_SCRIPTS_DIR/screenshots.zsh" [[ -f "$_SCRIPTS_DIR/claude-helpers.zsh" ]] && source "$_SCRIPTS_DIR/claude-helpers.zsh" [[ -f "$_SCRIPTS_DIR/git-helpers.zsh" ]] && source "$_SCRIPTS_DIR/git-helpers.zsh" + [[ -f "$_SCRIPTS_DIR/ssh-helpers.zsh" ]] && source "$_SCRIPTS_DIR/ssh-helpers.zsh" [[ -f "$_SCRIPTS_DIR/tmux-helpers.zsh" ]] && source "$_SCRIPTS_DIR/tmux-helpers.zsh" [[ -f "$_SCRIPTS_DIR/worktree-helpers.zsh" ]] && source "$_SCRIPTS_DIR/worktree-helpers.zsh" fi @@ -298,6 +299,8 @@ help-profile() { echo "\n${T}Claude Code${R}" echo " ${C}cc [path] ${R}Abrir Claude Code en directorio" echo " ${C}oc [path] ${R}Abrir OpenCode en directorio" + echo " ${C}zj [path] ${R}Entrar/crear sesión Zellij por repo/path" + echo " ${C}zsessions ${R}Listar sesiones Zellij" echo " ${C}ccx [p] ${R}Claude Code con contexto inicial" echo " ${C}ccd [sub] ${R}Navegar al Claude workspace" echo " ${C}ccclip ${R}Copiar código al clipboard" @@ -306,6 +309,16 @@ help-profile() { echo " ${C}zj [path] ${R}Entrar/crear sesión Zellij por repo/path" echo " ${C}zsessions ${R}Listar sesiones Zellij" echo " ${C}tcc ${R}Abrir Claude Code en sesión Zellij del repo" + + echo "\n${T}SSH / ubicación${R}" + echo " ${C}moshx [path] ${R}Mosh a host; con path entra al Zellij remoto del repo" + echo " ${C}moshx-doctor ${R}Verificar mosh-server, zellij, git y sh en el host" + echo " ${C}sshx ${R}SSH en sesión Zellij ssh-" + echo " ${C}sshc ${R}SSH directo con host visible en prompt" + echo " ${C}whereami ${R}Mostrar host, cwd, repo, sesión y SSH" + + echo "\n${T}Tmux — sesiones${R}" + echo " ${C}tcc ${R}Abrir Claude Code + lazygit (layout automático)" echo " ${C}tdev ${R}Sesión con nombre del directorio actual" echo " ${C}ta [nombre] ${R}Attach a sesión (o crearla)" echo " ${C}tn ${R}Nueva sesión con nombre" From cf45a2ebc144db3a7dc3a75848f2143b94647937 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Tue, 30 Jun 2026 14:35:46 +0000 Subject: [PATCH 5/6] feat: migrate terminal workflow from zellij/tmux to herdr --- CLAUDE.md | 5 +- README.md | 79 ++--- TODO.md | 7 +- ghostty/cmux.conf | 2 +- ghostty/config | 2 +- ghostty/muxy.conf | 2 +- herdr/config.toml | 25 ++ install.sh | 17 +- local/env.zsh.example | 4 +- starship/starship.toml | 10 +- tmux/tmux.conf | 116 ------- zellij/config.kdl | 78 ----- zellij/layouts/cortex.kdl | 15 - zellij/layouts/innit.kdl | 15 - zsh/scripts/claude-helpers.zsh | 543 +++---------------------------- zsh/scripts/herdr-helpers.zsh | 94 ++++++ zsh/scripts/ssh-helpers.zsh | 70 +--- zsh/scripts/tmux-helpers.zsh | 60 ---- zsh/scripts/worktree-helpers.zsh | 10 +- zsh/zshrc | 70 ++-- 20 files changed, 269 insertions(+), 955 deletions(-) create mode 100644 herdr/config.toml delete mode 100644 tmux/tmux.conf delete mode 100644 zellij/config.kdl delete mode 100644 zellij/layouts/cortex.kdl delete mode 100644 zellij/layouts/innit.kdl create mode 100644 zsh/scripts/herdr-helpers.zsh delete mode 100644 zsh/scripts/tmux-helpers.zsh diff --git a/CLAUDE.md b/CLAUDE.md index 8958c95..fdfe628 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,10 +32,11 @@ dotfiles/ │ ├── git-helpers.zsh # git-workdev, git-personaldev, git-whoami, clone-* │ ├── pcsoft-helpers.zsh # is-pcsoft-forbidden, is-pcsoft-editable │ ├── screenshots.zsh # ss, last, ssd, imgclip -│ └── tmux-helpers.zsh # ta, tn, tk, tl, tdev +│ ├── ssh-helpers.zsh # ssh/mosh con contexto visible +│ └── herdr-helpers.zsh # hhere, hremote, hname, whereami ├── starship/starship.toml # Prompt Gruvbox Dark → symlink a ~/.config/starship.toml ├── ghostty/config # Terminal → symlink a ~/.config/ghostty/config -├── tmux/tmux.conf # Multiplexor → symlink a ~/.tmux.conf +├── herdr/config.toml # Multiplexor → symlink a ~/.config/herdr/config.toml ├── local/ │ └── env.zsh.example # Template para local/env.zsh (gitignored) └── install.sh # Instalador diff --git a/README.md b/README.md index 43c3588..b34eea9 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,14 @@ Configuración local extraida de `cortex`: terminal, shell, prompt, helpers de A - **Terminal**: [Ghostty](https://ghostty.org/) - **Shell**: Zsh nativo de macOS - **Prompt**: [Starship](https://starship.rs/) — tema Gruvbox Dark -- **Multiplexor**: [Zellij](https://zellij.dev/) + helpers de sesión; tmux queda como compat/legacy +- **Multiplexor**: [Herdr](https://herdr.dev/) para sesiones locales/remotas persistentes - **Barra macOS**: [SketchyBar](https://github.com/FelixKratz/SketchyBar) con tema Gruvbox - **Window manager macOS**: [yabai](https://github.com/koekeishiya/yabai) + [skhd](https://github.com/koekeishiya/skhd) opcional y gradual - **Keyboard remaps macOS**: [Karabiner-Elements](https://karabiner-elements.pqrs.org/) con profile `cortex` - **Editor terminal**: [micro](https://micro-editor.github.io/) - **Editor principal**: Neovim basado en LazyVim/Gentleman.Dots con overlay RefactorIA - **Ls**: [eza](https://github.com/eza-community/eza) -- **AI CLI UX**: Claude Code statusline, OpenCode helpers y sesiones Zellij por repo +- **AI CLI UX**: Claude Code statusline, OpenCode helpers y sesiones Herdr por repo - **Supply-chain guardrails**: defaults globales para `uv`, `npm`, `pnpm` y `bun` - **Fuente**: FiraCode Nerd Font + variante custom RefactorIA @@ -29,7 +29,7 @@ bash install.sh ``` El instalador macOS: -1. Instala dependencias via Homebrew (starship, tmux, zellij, mosh, lazygit, micro, eza, sketchybar, yabai, skhd, Karabiner-Elements, FiraCode Nerd Font) +1. Instala dependencias via Homebrew (starship, herdr, mosh, lazygit, micro, eza, sketchybar, yabai, skhd, Karabiner-Elements, FiraCode Nerd Font) 2. Hace backup de configs existentes con timestamp 3. Crea symlinks de los dotfiles y guardrails globales (`.npmrc`, `pnpm/rc`, `.bunfig.toml`, `uv.toml`) 4. Intenta seleccionar el profile `cortex` de Karabiner si `karabiner_cli` está disponible @@ -43,7 +43,7 @@ dotfiles/ ├── claude/ # Claude Code statusline ├── ghostty/ # Config Ghostty y shaders ├── fonts/ # Fuente RefactorIA y script de regeneración -├── zellij/ # Theme/layout Zellij InnIT +├── herdr/ # Config Herdr ├── npm/ # Global npm defaults (~/.npmrc) ├── pnpm/ # Global pnpm defaults (~/Library/Preferences/pnpm/rc) ├── bun/ # Global bun defaults (~/.bunfig.toml) @@ -53,12 +53,11 @@ dotfiles/ │ └── scripts/ │ ├── claude-helpers.zsh # Integración Claude Code │ ├── git-helpers.zsh # Identidades Git y clone helpers -│ ├── ssh-helpers.zsh # SSH/Mosh con contexto visible y sesiones Zellij remotas por repo -│ ├── tmux-helpers.zsh # Compat aliases t* sobre Zellij +│ ├── ssh-helpers.zsh # SSH/Mosh con contexto visible +│ ├── herdr-helpers.zsh # Helpers Herdr para sesiones y orientación │ ├── worktree-helpers.zsh # Helpers git worktree │ ├── screenshots.zsh # Manejo de screenshots macOS │ └── pcsoft-helpers.zsh # Protección archivos PCSoft -├── tmux/ # Config tmux legacy, no enlazada por default ├── lazygit/ # Config lazygit ├── karabiner/ # Config Karabiner-Elements (~/.config/karabiner/karabiner.json) ├── micro/ # Settings y themes de micro @@ -83,15 +82,15 @@ dotfiles/ | `cortex`, `dotfiles` | Navegación rápida al repo `cortex` y sus dotfiles | | `cc [path]` | Abrir Claude Code | | `oc [path]` | Abrir OpenCode | -| `zj [path]` | Entrar/crear sesión Zellij por repo/path | -| `zsessions` | Listar sesiones Zellij | -| `moshx [remote-path]` | Mosh al host; con path entra al Zellij remoto del repo | -| `moshx-doctor ` | Verifica `mosh-server`, `zellij`, `git` y `sh` en el host remoto | -| `sshx ` | SSH en sesión Zellij `ssh-` | +| `hhere [path]` | Entrar/crear sesión Herdr por host+repo+branch | +| `hremote [session]` | Attach remoto con `herdr --remote` | +| `hname [label]` | Nombrar el pane Herdr actual | +| `moshx [remote-path]` | Mosh al host; con path entra a ese directorio remoto | +| `moshx-doctor ` | Verifica `mosh-server`, `herdr`, `git` y `sh` en el host remoto | +| `sshx ` | SSH directo con contexto visible | | `sshc ` | SSH directo con host visible en prompt | | `whereami` | Mostrar host, cwd, repo, sesión y SSH | | `ccclip ` | Copiar código al clipboard | -| `tcc`, `tdev`, `ta`, `tn`, `tl`, `tk` | Helpers Zellij compatibles con la memoria muscular tmux | | `wtadd`, `wtlist`, `wtremove` | Helpers de git worktrees | | `ss [n]` | Listar últimos screenshots | | `last [-c\|-o]` | Último screenshot | @@ -107,55 +106,33 @@ Editá `local/env.zsh` (gitignored) para configurar: - `SCREENSHOTS_DIR` — directorio de screenshots - `WORKSPACE_DIR` — directorio raíz de tus proyectos - `OPENCODE_DEFAULT_FLAGS` — flags por defecto para `oc` -- `CORTEX_MULTIPLEXER=zellij` — usa Zellij para `cc`/`oc`; es el default del profile +- `CORTEX_MULTIPLEXER=herdr` — marca Herdr como multiplexor operativo para prompt/helpers - `INNIT_DIR` y overrides `INNIT_*_DIR` — navegación rápida de subdirectorios - Aliases y paths personales -## Zellij +## Herdr remoto -El modelo recomendado es una sesión Zellij por repo: +Para SSH/remoto, el modelo recomendado es Herdr. Usá un workspace por repo, tabs por objetivo y panes por agente/proceso: ```bash -zj ~/dev/personal/infra -zj ~/dev/personal/cortex +hremote agent-dev-01 main +hhere ~/dev/personal/cortex ``` -`cc [path]`, `ccb [path]`, `oc [path]` y `ocb [path]` usan Zellij por default: +Si necesitás diagnosticar dependencias: ```bash -export CORTEX_MULTIPLEXER="zellij" +moshx-doctor agent-dev-01 ``` Comportamiento: -- Si estás en la sesión del repo actual, ejecuta el agente en el pane actual. -- Si pedís otro path y la sesión ya existe, cambia a esa sesión. -- Si pedís otro path y la sesión no existe, la crea con el agente arrancado en ese directorio. -- Las sesiones se nombran con contexto visible: `local::` o `ssh::`. -- El layout `innit` muestra tab bar arriba y status bar abajo usando el theme `innit`. -- Si necesitás evitar Zellij puntualmente, seteá `CORTEX_MULTIPLEXER` vacío en esa shell y ejecutá el agente directo. - -La config versionada de Zellij prioriza orientación: - -- Barra superior con tabs y barra inferior de estado siempre visibles. -- Pane frames activados para ver límites y foco. -- Prompt Starship marca `zj:` cuando estás dentro de Zellij. -- `whereami` y `zwhere` muestran ubicación completa sin depender de la UI. - -Para workstation remota persistente, la unidad principal es una sesión Zellij por repo en el host remoto. Mosh es sólo el transporte resiliente: - -```bash -moshx agent-dev-01 ~/dev/personal/cortex -moshx agent-dev-01 ~/dev/personal/infra -``` - -Cada comando entra por Mosh y hace attach/create de una sesión Zellij remota nombrada por el repo. Para hosts sin Mosh o troubleshooting, `sshx agent-dev-01` queda como fallback SSH clásico. - -Si falla, diagnosticá primero: - -```bash -moshx-doctor agent-dev-01 -``` +- `hhere [path]` nombra la sesión por host + repo + branch. +- `hremote [session]` usa el bridge remoto de Herdr. +- `hname [label]` evita panes anónimos en el sidepanel. +- `cc [path]`, `ccb [path]`, `oc [path]` y `ocb [path]` ejecutan el agente en el pane actual; Herdr provee persistencia. +- Prompt Starship marca `herdr` cuando `CORTEX_MULTIPLEXER=herdr`. +- `whereami` muestra ubicación completa sin depender de la UI. ## SketchyBar @@ -166,9 +143,9 @@ Layout activo: | Pantalla | Uso | Layout | |------|-----|--------| | Mac Retina (`display=1`) | apps generales: Discord, WhatsApp, Mail, Postman, Zen Browser | app activa + network, volumen, calendario, hora, batería | -| ViewSonic vertical (`display=2`) | auxiliar/random, Ghostty/Zellij y Claude de formato vertical | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | -| LG Ultrawide (`display=3`) | mixto: Ghostty/Zellij, Claude, ChatGPT, Obsidian | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | -| 4K derecho (`display=4`) | Ghostty/Zellij exclusivo | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | +| ViewSonic vertical (`display=2`) | auxiliar/random, Ghostty/Herdr y Claude de formato vertical | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | +| LG Ultrawide (`display=3`) | mixto: Ghostty/Herdr, Claude, ChatGPT, Obsidian | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | +| 4K derecho (`display=4`) | Ghostty/Herdr exclusivo | brand + panel/spaces + app activa + git + issue/PR + SDD + brains + timer; derecha: RAM + CPU + hora | El centro queda libre para evitar el notch y reducir ruido visual. diff --git a/TODO.md b/TODO.md index 1522e30..d21775e 100644 --- a/TODO.md +++ b/TODO.md @@ -8,8 +8,6 @@ Mejoras identificadas en dotfiles externos de referencia. - [x] **Glyph custom RefactorIA para Nerd Font** — `FiraCode Nerd Font Mono Beard` versionado en `fonts/FiraCodeNerdFontMonoBeard-Reg.ttf`, instalado en `~/Library/Fonts/FiraCodeNerdFontMonoBeard-Reg.ttf`, codepoint `U+F0F00`, script regenerable en `fonts/patch_beard.py`, Ghostty y Starship configurados. - [x] **Statusline custom para Claude Code** — script que muestra barra visual de uso del contexto (verde/amarillo/rojo), modelo activo, rama git y porcentaje exacto. -- [x] **Sesión flotante Alt+G en tmux** — popup flotante sobre cualquier layout. -- [x] **tmux-resurrect** — guarda y restaura sesiones tmux al reiniciar. `Prefix+Ctrl+S` / `Prefix+Ctrl+R`. ## Media prioridad @@ -23,7 +21,6 @@ Mejoras identificadas en dotfiles externos de referencia. ## Baja prioridad - [x] **Shaders de cursor en Ghostty** — 4 shaders disponibles en `ghostty/shaders/`. Activo: cursor_smear_gentleman. Para cambiar: editar `custom-shader` en ghostty/config. -- [x] **`tmux-which-key`** — muestra keybindings al presionar el prefix. - [x] **`window-padding-balance = true` en Ghostty** — padding balanceado en splits. --- @@ -44,6 +41,6 @@ Mejoras identificadas en dotfiles externos de referencia. ## Worktrees — integración natural con Claude -- [x] **Worktree helpers zsh + cmux** — `wtadd`/`wtlist`/`wtremove` en `worktree-helpers.zsh`. Mecánica pura: crear directorio hermano, detectar repos PCSoft via `is-pcsoft-forbidden`, abrir workspace cmux automáticamente. Los comandos son la infraestructura, no el punto de entrada al usuario. +- [x] **Worktree helpers zsh + Herdr** — `wtadd`/`wtlist`/`wtremove` en `worktree-helpers.zsh`. Mecánica pura: crear directorio hermano, detectar repos PCSoft via `is-pcsoft-forbidden`, abrir agente en el flujo Herdr. Los comandos son la infraestructura, no el punto de entrada al usuario. -- [x] **Regla de evaluación proactiva en claude-config** — Claude evalúa si worktrees convienen y los sugiere/implementa sin que el usuario lo pida. Triggers: "hay un bug urgente y estoy en medio de algo", feature branch larga + hotfix simultáneo, "no quiero perder contexto pero necesito cambiar de rama". Si el repo es PCSoft → nunca sugerir. Si es no-PCSoft y hay conflicto de contexto → proponer `wtadd` + nuevo workspace cmux directamente. +- [x] **Regla de evaluación proactiva en claude-config** — Claude evalúa si worktrees convienen y los sugiere/implementa sin que el usuario lo pida. Triggers: "hay un bug urgente y estoy en medio de algo", feature branch larga + hotfix simultáneo, "no quiero perder contexto pero necesito cambiar de rama". Si el repo es PCSoft → nunca sugerir. Si es no-PCSoft y hay conflicto de contexto → proponer `wtadd` + nuevo workspace Herdr directamente. diff --git a/ghostty/cmux.conf b/ghostty/cmux.conf index aedabe6..1482af1 100644 --- a/ghostty/cmux.conf +++ b/ghostty/cmux.conf @@ -51,7 +51,7 @@ cursor-style = block_hollow # Integración con shell shell-integration = zsh -# Option izquierdo como Alt para Zellij/tmux; Option derecho conserva tildes/ñ. +# Option izquierdo como Alt para TUIs; Option derecho conserva tildes/ñ. macos-option-as-alt = left # Comportamiento diff --git a/ghostty/config b/ghostty/config index 3740eb4..90be0bd 100644 --- a/ghostty/config +++ b/ghostty/config @@ -52,7 +52,7 @@ cursor-style = block_hollow # Integración con shell shell-integration = zsh -# Option izquierdo como Alt para Zellij/tmux; Option derecho conserva tildes/ñ. +# Option izquierdo como Alt para TUIs; Option derecho conserva tildes/ñ. macos-option-as-alt = left # Comportamiento diff --git a/ghostty/muxy.conf b/ghostty/muxy.conf index c0bbb68..e9b4e92 100644 --- a/ghostty/muxy.conf +++ b/ghostty/muxy.conf @@ -24,7 +24,7 @@ cursor-style = block_hollow # Integración con shell shell-integration = zsh -# Option izquierdo como Alt para Zellij/tmux; Option derecho conserva tildes/ñ. +# Option izquierdo como Alt para TUIs; Option derecho conserva tildes/ñ. macos-option-as-alt = left # Comportamiento diff --git a/herdr/config.toml b/herdr/config.toml new file mode 100644 index 0000000..6704915 --- /dev/null +++ b/herdr/config.toml @@ -0,0 +1,25 @@ +[ui.toast] +delivery = "terminal" + +[ui] +sidebar_width = 32 +sidebar_min_width = 24 +sidebar_max_width = 42 +agent_panel_sort = "priority" +show_agent_labels_on_pane_borders = true +prompt_new_tab_name = true +pane_borders = true +pane_gaps = true + +[terminal] +new_cwd = "follow" + +[session] +resume_agents_on_restore = true + +[remote] +manage_ssh_config = true + +[theme] +name = "gruvbox" +auto_switch = false diff --git a/install.sh b/install.sh index 94a7fed..c13c56d 100755 --- a/install.sh +++ b/install.sh @@ -68,8 +68,7 @@ karabiner_cli_path() { install_with_brew starship starship "starship" install_with_brew micro micro "micro (editor terminal)" install_with_brew eza eza "eza (ls mejorado)" -install_with_brew tmux tmux "tmux (multiplexor de terminal)" -install_with_brew zellij zellij "zellij (multiplexor remoto persistente)" +install_with_brew herdr herdr "herdr (multiplexor remoto persistente)" install_with_brew mosh mosh "mosh (SSH resiliente para workstations remotas)" install_with_brew lazygit lazygit "lazygit (git TUI)" @@ -151,10 +150,7 @@ backup_if_exists "$HOME/.npmrc" backup_if_exists "$HOME/.bunfig.toml" backup_if_exists "$HOME/.config/uv/uv.toml" backup_if_exists "$HOME/.config/starship.toml" -backup_if_exists "$HOME/.config/zellij/config.kdl" -backup_if_exists "$HOME/.config/zellij/layouts/innit.kdl" -backup_if_exists "$HOME/.config/zellij/layouts/cortex.kdl" -backup_if_exists "$HOME/.tmux.conf" +backup_if_exists "$HOME/.config/herdr/config.toml" backup_if_exists "$HOME/.claude/statusline.sh" backup_if_exists "$HOME/.config/lazygit/config.yml" @@ -191,10 +187,7 @@ create_symlink "$DOTFILES/npm/npmrc" "$HOME/.npmrc" create_symlink "$DOTFILES/bun/bunfig.toml" "$HOME/.bunfig.toml" create_symlink "$DOTFILES/uv/uv.toml" "$HOME/.config/uv/uv.toml" create_symlink "$DOTFILES/starship/starship.toml" "$HOME/.config/starship.toml" -create_symlink "$DOTFILES/zellij/config.kdl" "$HOME/.config/zellij/config.kdl" -create_symlink "$DOTFILES/zellij/layouts/innit.kdl" "$HOME/.config/zellij/layouts/innit.kdl" -create_symlink "$DOTFILES/zellij/layouts/cortex.kdl" "$HOME/.config/zellij/layouts/cortex.kdl" -create_symlink "$DOTFILES/tmux/tmux.conf" "$HOME/.tmux.conf" +create_symlink "$DOTFILES/herdr/config.toml" "$HOME/.config/herdr/config.toml" chmod +x "$DOTFILES/claude/statusline.sh" create_symlink "$DOTFILES/claude/statusline.sh" "$HOME/.claude/statusline.sh" create_symlink "$DOTFILES/micro/settings.json" "$HOME/.config/micro/settings.json" @@ -309,12 +302,12 @@ echo " 1. Abrí una nueva shell para cargar el nuevo profile" echo " 2. Editá local/env.zsh con tus paths personales" if $IS_MACOS; then echo " 3. Ghostty ya usa FiraCode Nerd Font Mono Beard (reiniciá si no se ve bien)" - echo " 4. Usá zj, cc u oc para abrir sesiones Zellij por repo" + echo " 4. Usá hhere, hremote, cc u oc para trabajar dentro de Herdr" echo " 5. Si macOS bloqueó servicios, habilitá Accessibility y corré los fallbacks impresos arriba" echo " 6. Abrí Karabiner-Elements y habilitá Input Monitoring/Accessibility si macOS lo pide" else echo " 3. Instalá manualmente herramientas faltantes que el script haya marcado con !" - echo " 4. Usá zj, cc u oc para abrir sesiones Zellij por repo" + echo " 4. Usá hhere, hremote, cc u oc para trabajar dentro de Herdr" fi echo "" echo " Para medir el load time:" diff --git a/local/env.zsh.example b/local/env.zsh.example index d48a36a..4652392 100644 --- a/local/env.zsh.example +++ b/local/env.zsh.example @@ -16,8 +16,8 @@ # OpenCode # export OPENCODE_DEFAULT_FLAGS="" -# Multiplexor preferido para cc/oc. Zellij es el default del profile. -# export CORTEX_MULTIPLEXER="zellij" +# Multiplexor preferido para cc/oc y sesiones remotas. +# export CORTEX_MULTIPLEXER="herdr" # Navegación rápida para proyectos de una organización/equipo # export INNIT_DIR="$WORKSPACE_DIR/innit" diff --git a/starship/starship.toml b/starship/starship.toml index 09578d7..877cbb7 100644 --- a/starship/starship.toml +++ b/starship/starship.toml @@ -1,7 +1,7 @@ # Formato general format = """ ${custom.ssh_context}\ -${custom.zellij_context}\ +${custom.herdr_context}\ ${custom.repo}\ [|](fg:color_orange)\ $directory\ @@ -100,11 +100,11 @@ command = "printf 'ssh:%s' \"${CORTEX_SSH_TARGET:-$(hostname -s 2>/dev/null || h style = "fg:color_fg0 bg:color_red" format = '[ $output ]($style)[|](fg:color_red)' -[custom.zellij_context] -description = "Sesión Zellij visible en el prompt" -when = "test -n \"$ZELLIJ\"" +[custom.herdr_context] +description = "Herdr como multiplexor activo" +when = "test \"${CORTEX_MULTIPLEXER:-}\" = herdr" shell = ["bash", "--noprofile", "--norc"] -command = "printf 'zj:%s' \"${ZELLIJ_SESSION_NAME:-?}\" | cut -c1-28" +command = "printf 'herdr'" style = "fg:color_fg0 bg:color_purple" format = '[ $output ]($style)[|](fg:color_purple)' diff --git a/tmux/tmux.conf b/tmux/tmux.conf deleted file mode 100644 index e2f568e..0000000 --- a/tmux/tmux.conf +++ /dev/null @@ -1,116 +0,0 @@ -# tmux.conf — Dev Environment -# Prefix: Ctrl+A - -#region Prefix -unbind C-b -set -g prefix C-a -bind C-a send-prefix -#endregion - -#region Terminal -# True color para que los colores del tema funcionen -set -g default-terminal "tmux-256color" -set -ag terminal-overrides ",xterm-256color:RGB" -set -as terminal-features ",*:RGB" -set -as terminal-features ",*:usstyle" -set -as terminal-features ",*:hyperlinks" - -# Propagar extended keys (shift+enter, ctrl+space, etc.) a apps que las soportan -# Requiere terminal exterior con kitty keyboard protocol (Ghostty/muxy/iTerm2/WezTerm/Kitty) -set -g extended-keys on -set -s extended-keys-format csi-u -set -ga terminal-features 'xterm*:extkeys' -#endregion - -#region General -# Numeración desde 1 (más cómodo que 0) -set -g base-index 1 -setw -g pane-base-index 1 -set -g renumber-windows on - -# Sin delay en ESC (bueno para micro/nvim) -set -s escape-time 0 - -# Historia larga -set -g history-limit 50000 - -# Mouse habilitado -set -g mouse on - -# Título de la ventana del terminal -set -g set-titles on -set -g set-titles-string "#S / #W" -#endregion - -#region Vi Keys -# Copy mode con vi keys -setw -g mode-keys vi -bind -T copy-mode-vi v send -X begin-selection -bind -T copy-mode-vi y send -X copy-pipe-and-cancel "pbcopy" -bind -T copy-mode-vi Escape send -X cancel -#endregion - -#region Window/Pane Management -# Splits que mantienen el directorio actual -bind | split-window -h -c "#{pane_current_path}" -bind - split-window -v -c "#{pane_current_path}" -bind c new-window -c "#{pane_current_path}" - -# Navegación entre panes con hjkl -bind h select-pane -L -bind j select-pane -D -bind k select-pane -U -bind l select-pane -R - -# Resize de panes -bind -r H resize-pane -L 5 -bind -r J resize-pane -D 5 -bind -r K resize-pane -U 5 -bind -r L resize-pane -R 5 -#endregion - -#region Reload -bind r source-file ~/.tmux.conf \; display " Config recargada" -#endregion - -#region Scratch Session -# Ventana flotante con Alt+G — sobre cualquier layout, sin romper el contexto actual -bind-key -n M-g if-shell -F '#{==:#{session_name},scratch}' { - detach-client -} { - display-popup -d "#{pane_current_path}" -E "tmux new-session -A -s scratch" -} -#endregion - -#region Plugins -set -g @plugin 'tmux-plugins/tpm' -set -g @plugin 'tmux-plugins/tmux-resurrect' -set -g @plugin 'alexwforsythe/tmux-which-key' -#endregion - -#region Status Bar — Dark -set -g status-style "bg=#0c0c0c,fg=#ffffff" -set -g status-position bottom -set -g status-interval 5 - -# Izquierda: nombre de sesión -set -g status-left "#[fg=#16c60c,bold] #S #[default] " -set -g status-left-length 30 - -# Derecha: hora y fecha -set -g status-right "#[fg=#555555]%H:%M %d/%m " - -# Ventanas -set -g window-status-format "#[fg=#555555] #I:#W " -set -g window-status-current-format "#[fg=#16c60c,bold] #I:#W " - -# Bordes de panes -set -g pane-border-style "fg=#333333" -set -g pane-active-border-style "fg=#16c60c" - -# Mensajes -set -g message-style "bg=#16c60c,fg=#0c0c0c,bold" -#endregion - -# Inicializar TPM (debe ser la última línea del archivo) -run '~/.tmux/plugins/tpm/tpm' diff --git a/zellij/config.kdl b/zellij/config.kdl deleted file mode 100644 index 3651905..0000000 --- a/zellij/config.kdl +++ /dev/null @@ -1,78 +0,0 @@ -theme "innit" - -// Zellij config — orientation-first UX for local and remote dev. -// Goal: always know session, tab, pane, host and path at a glance. - -simplified_ui true -pane_frames true -default_mode "normal" -mouse_mode true -copy_on_select true -scroll_buffer_size 20000 -session_serialization true -pane_viewport_serialization true -scrollback_lines_to_serialize 5000 - -ui { - pane_frames { - rounded_corners true - } -} - -themes { - innit { - fg 248 250 252 - bg 11 17 24 - black 0 0 0 - red 239 68 68 - green 63 185 80 - yellow 245 158 11 - blue 56 189 248 - magenta 160 32 240 - cyan 58 150 221 - white 248 250 252 - orange 245 158 11 - - ribbon_selected { - base 248 250 252 - background 63 185 80 - emphasis_0 11 17 24 - emphasis_1 56 189 248 - emphasis_2 245 158 11 - emphasis_3 239 68 68 - } - - ribbon_unselected { - base 148 163 184 - background 16 24 32 - emphasis_0 63 185 80 - emphasis_1 56 189 248 - emphasis_2 245 158 11 - emphasis_3 239 68 68 - } - } -} - -keybinds { - normal { - bind "Alt h" "Alt Left" { MoveFocusOrTab "Left"; } - bind "Alt j" "Alt Down" { MoveFocus "Down"; } - bind "Alt k" "Alt Up" { MoveFocus "Up"; } - bind "Alt l" "Alt Right" { MoveFocusOrTab "Right"; } - bind "Alt n" { NewPane; } - bind "Alt |" { NewPane "Right"; } - bind "Alt -" { NewPane "Down"; } - bind "Alt f" { ToggleFocusFullscreen; } - bind "Alt s" { SwitchToMode "session"; } - bind "Alt r" { RenameSession; } - bind "Alt t" { NewTab; } - bind "Alt 1" { GoToTab 1; } - bind "Alt 2" { GoToTab 2; } - bind "Alt 3" { GoToTab 3; } - bind "Alt 4" { GoToTab 4; } - bind "Alt 5" { GoToTab 5; } - bind "Alt ?" { SwitchToMode "tmux"; } - } -} - -default_layout "innit" diff --git a/zellij/layouts/cortex.kdl b/zellij/layouts/cortex.kdl deleted file mode 100644 index e02e015..0000000 --- a/zellij/layouts/cortex.kdl +++ /dev/null @@ -1,15 +0,0 @@ -layout { - default_tab_template { - pane size=1 borderless=true { - plugin location="zellij:tab-bar" - } - children - pane size=2 borderless=true { - plugin location="zellij:status-bar" - } - } - - tab name="shell" focus=true { - pane - } -} diff --git a/zellij/layouts/innit.kdl b/zellij/layouts/innit.kdl deleted file mode 100644 index e02e015..0000000 --- a/zellij/layouts/innit.kdl +++ /dev/null @@ -1,15 +0,0 @@ -layout { - default_tab_template { - pane size=1 borderless=true { - plugin location="zellij:tab-bar" - } - children - pane size=2 borderless=true { - plugin location="zellij:status-bar" - } - } - - tab name="shell" focus=true { - pane - } -} diff --git a/zsh/scripts/claude-helpers.zsh b/zsh/scripts/claude-helpers.zsh index 8ee8dda..0c0a5c9 100644 --- a/zsh/scripts/claude-helpers.zsh +++ b/zsh/scripts/claude-helpers.zsh @@ -1,14 +1,5 @@ #region Claude Code Helpers -# Funciones de integración con Claude Code CLI - -_cmux_sidebar_refresh() { - local target="${1:-$PWD}" - local dotfiles_dir="${_DOTFILES_DIR:-$HOME/dev/personal/cortex-dotfiles}" - local script="$dotfiles_dir/zsh/scripts/cmux-sidebar-refresh.sh" - if [[ -x "$script" ]]; then - "$script" "$target" >/dev/null 2>&1 || true - fi -} +# Funciones de integración con Claude Code/OpenCode. Herdr maneja sesiones, panes y persistencia. _workspace_name_for_path() { local target="${1:-$PWD}" @@ -16,8 +7,7 @@ _workspace_name_for_path() { git_root=$(git -C "$target" rev-parse --show-toplevel 2>/dev/null || true) if [[ -n "$git_root" ]]; then - local repo_name - local parent_name + local repo_name parent_name repo_name=$(basename "$git_root" | tr '.' '-') parent_name=$(basename "$(dirname "$git_root")" | tr '.' '-') printf '%s-%s' "$parent_name" "$repo_name" @@ -26,461 +16,43 @@ _workspace_name_for_path() { fi } -_zellij_context_label() { - local host - host="${HOST%%.*}" - host="${host:-$(hostname -s 2>/dev/null)}" - - if [[ -n "$SSH_CONNECTION" || -n "$SSH_CLIENT" || -n "$SSH_TTY" ]]; then - printf 'ssh:%s' "$host" - else - printf 'local:%s' "$host" - fi -} - -_zellij_session_name_for_path() { - printf '%s:%s' "$(_zellij_context_label)" "$(_workspace_name_for_path "${1:-$PWD}")" -} - -_zellij_default_layout() { - local layout_file="${_DOTFILES_DIR:-$HOME/dev/personal/cortex-dotfiles}/zellij/layouts/innit.kdl" - [[ -f "$layout_file" ]] && printf '%s' "$layout_file" -} - -_zellij_preferred() { - [[ -n "$ZELLIJ" || "${CORTEX_MULTIPLEXER:-}" == "zellij" ]] -} - -_zellij_available() { - command -v zellij >/dev/null 2>&1 -} - -_zellij_session_exists() { - local session="$1" - zellij list-sessions --short --no-formatting 2>/dev/null | grep -Fxq "$session" -} - -_zellij_session_exited() { - local session="$1" - zellij list-sessions --no-formatting 2>/dev/null | grep -Eq "^${session}[[:space:]].*EXITED" -} - -_zellij_recreate_if_exited() { - local session="$1" - if _zellij_session_exited "$session"; then - zellij delete-session "$session" >/dev/null 2>&1 || true - fi -} - -_zellij_kdl_escape() { - local value="$1" - value="${value//\\/\\\\}" - value="${value//\"/\\\"}" - printf '%s' "$value" -} - -_zellij_agent_session_name() { - local resolved="$1" - local agent="$2" - local base - base="$(_workspace_name_for_path "$resolved")" - - if [[ -n "$agent" ]]; then - printf '%s-%s' "$base" "$agent" - else - printf '%s' "$base" - fi -} - -_zellij_layout_for_command() { - local resolved="$1" - local command_line="$2" - local title="${3:-$(basename "$resolved")}" - local layout_file - layout_file=$(mktemp "${TMPDIR:-/tmp}/cortex-zellij-layout.XXXXXX.kdl") - - local cwd_escaped command_escaped shell_name title_escaped - cwd_escaped="$(_zellij_kdl_escape "$resolved")" - command_escaped="$(_zellij_kdl_escape "cd ${(q)resolved} && $command_line; exec ${SHELL:-zsh}")" - shell_name="$(_zellij_kdl_escape "${SHELL:-zsh}")" - title_escaped="$(_zellij_kdl_escape "$title")" - - cat > "$layout_file" </dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" - return 1 - fi - - if ! command -v zellij >/dev/null 2>&1; then - echo "❌ zellij no está instalado" - return 1 - fi - - local session - session="$(_zellij_session_name_for_path "$resolved")" - - if [[ -n "$ZELLIJ" ]]; then - if _zellij_session_exists "$session"; then - zellij action switch-session -c "$resolved" "$session" - else - local layout_file - layout_file="$(_zellij_default_layout)" - if [[ -n "$layout_file" ]]; then - zellij action switch-session -c "$resolved" --layout "$layout_file" "$session" - else - zellij action switch-session -c "$resolved" "$session" - fi - fi - else - local layout_file - layout_file="$(_zellij_default_layout)" - if _zellij_session_exists "$session"; then - zellij attach "$session" - elif [[ -n "$layout_file" ]]; then - cd "$resolved" && zellij --session "$session" --layout "$layout_file" - else - cd "$resolved" && zellij attach --create "$session" - fi - fi -} - -zsessions() { - zellij list-sessions --no-formatting -} - -zwhere() { - if [[ -z "$ZELLIJ" ]]; then - echo "No estás dentro de Zellij" - return 1 - fi - - echo "Zellij: ${ZELLIJ_SESSION_NAME:-unknown}" - echo "Path: $PWD" - if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo "Repo: $(git rev-parse --show-toplevel)" - echo "Branch: $(git branch --show-current 2>/dev/null || printf detached)" - fi -} - -zn() { - local target="${1:-.}" - local agent="${2:-}" - local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" - return 1 - fi - - if [[ -z "$ZELLIJ" ]]; then - echo "❌ zn solo funciona dentro de Zellij" + resolved=$(cd -q "$target" >/dev/null 2>&1 && pwd) || { + echo "Directorio no encontrado: $target" return 1 - fi - - zellij action rename-session "$(_zellij_agent_session_name "$resolved" "$agent")" -} - -_cmux_rename_workspace() { - local workspace_id="$1" - local workspace_name="$2" - - if [[ -z "$workspace_id" || -z "$workspace_name" ]]; then - return 0 - fi - - if command -v cmux >/dev/null 2>&1; then - # env -u descarta el socket heredado del proceso padre (cc/oc/ccb/ocb operan sobre la - # intención explícita del usuario — el workspace destino que indicó al lanzar el comando —, - # no sobre el focused actual; por eso solo aplicamos env -u sin --no-caller) - env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux rename-workspace --workspace "$workspace_id" "$workspace_name" >/dev/null 2>&1 || true - fi + } + printf '%s' "$resolved" } -# Abrir Claude Code en Zellij o en el directorio actual. +# Abrir Claude Code en el directorio actual/pasado. cc() { - local target="${1:-.}" local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" - return 1 - fi - - if _zellij_preferred && _zellij_available; then - _zellij_open_agent "$resolved" "claude --enable-auto-mode --dangerously-skip-permissions" "claude" - elif _zellij_preferred; then - echo "⚠️ zellij no está instalado; ejecutando Claude Code directo" - cd "$resolved" && claude --enable-auto-mode --dangerously-skip-permissions - elif [[ -n "$CMUX_WORKSPACE_ID" ]]; then - # Estamos dentro de cmux - local workspace_name - workspace_name="$(_workspace_name_for_path "$resolved")" - - # Si el directorio objetivo es el actual, lanzar claude aquí mismo - if [[ "$resolved" == "$PWD" ]]; then - _cmux_rename_workspace "$CMUX_WORKSPACE_ID" "$workspace_name" - _cmux_sidebar_refresh "$resolved" - claude --enable-auto-mode --dangerously-skip-permissions - return - fi - - # Directorio diferente: verificar si ya existe un workspace para no duplicar - # env -u descarta el socket heredado: cc opera sobre el dir/workspace destino que el - # usuario indicó explícitamente (no sobre el focused), por eso solo env -u, sin --no-caller - local existing_id - existing_id=$(env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux list-workspaces 2>/dev/null | jq -r --arg name "$workspace_name" '.[] | select(.title == $name) | .id' 2>/dev/null | head -1) - - if [[ -n "$existing_id" ]]; then - # El workspace ya existe: enfocarlo sin crear uno nuevo - _cmux_rename_workspace "$existing_id" "$workspace_name" - env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux select-workspace --workspace "$existing_id" - _cmux_sidebar_refresh "$resolved" - else - # No existe: crear workspace nuevo con claude corriendo - if ! env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux new-workspace --name "$workspace_name" --cwd "$resolved" --command "claude --enable-auto-mode --dangerously-skip-permissions"; then - echo "⚠️ cmux new-workspace falló, ejecutando claude en el directorio actual" - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && claude --enable-auto-mode --dangerously-skip-permissions - fi - fi - elif [[ -n "$TMUX" ]]; then - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && claude --enable-auto-mode --dangerously-skip-permissions - else - cd "$resolved" && claude --enable-auto-mode --dangerously-skip-permissions - fi + resolved=$(_resolve_dir_or_fail "${1:-.}") || return 1 + cd "$resolved" && claude --enable-auto-mode --dangerously-skip-permissions } -# Abrir OpenCode en Zellij/cmux o en el directorio actual. -# Mantiene el mismo patrón de uso que cc() pero usando opencode +# Abrir OpenCode en el directorio actual/pasado. oc() { - local target="${1:-.}" local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" - return 1 - fi - - local oc_cmd="opencode ${OPENCODE_DEFAULT_FLAGS:-}" - - if _zellij_preferred && _zellij_available; then - _zellij_open_agent "$resolved" "$oc_cmd" "opencode" - elif _zellij_preferred; then - echo "⚠️ zellij no está instalado; ejecutando OpenCode directo" - cd "$resolved" && eval "$oc_cmd" - elif [[ -n "$CMUX_WORKSPACE_ID" ]]; then - local workspace_name - workspace_name="$(_workspace_name_for_path "$resolved")" - - if [[ "$resolved" == "$PWD" ]]; then - _cmux_rename_workspace "$CMUX_WORKSPACE_ID" "$workspace_name" - _cmux_sidebar_refresh "$resolved" - eval "$oc_cmd" - return - fi - - # env -u descarta el socket heredado: oc opera sobre el dir/workspace destino que el - # usuario indicó explícitamente (no sobre el focused), por eso solo env -u, sin --no-caller - local existing_id - existing_id=$(env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux list-workspaces 2>/dev/null | jq -r --arg name "$workspace_name" '.[] | select(.title == $name) | .id' 2>/dev/null | head -1) - - if [[ -n "$existing_id" ]]; then - _cmux_rename_workspace "$existing_id" "$workspace_name" - env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux select-workspace --workspace "$existing_id" - _cmux_sidebar_refresh "$resolved" - else - if ! env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux new-workspace --name "$workspace_name" --cwd "$resolved" --command "$oc_cmd"; then - echo "⚠️ cmux new-workspace falló, ejecutando OpenCode en el directorio actual" - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$oc_cmd" - fi - fi - elif [[ -n "$TMUX" ]]; then - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$oc_cmd" - else - cd "$resolved" && eval "$oc_cmd" - fi + resolved=$(_resolve_dir_or_fail "${1:-.}") || return 1 + cd "$resolved" && opencode ${OPENCODE_DEFAULT_FLAGS:-} } -# Abrir Claude Code con bypass de permisos explícito +# Abrir Claude Code con bypass de permisos explícito. ccb() { - local target="${1:-.}" local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" - return 1 - fi - - local cc_cmd="claude --dangerously-skip-permissions" - - if _zellij_preferred && _zellij_available; then - _zellij_open_agent "$resolved" "$cc_cmd" "claude" - elif _zellij_preferred; then - echo "⚠️ zellij no está instalado; ejecutando Claude Code directo" - cd "$resolved" && eval "$cc_cmd" - elif [[ -n "$CMUX_WORKSPACE_ID" ]]; then - local workspace_name - workspace_name="$(_workspace_name_for_path "$resolved")" - - if [[ "$resolved" == "$PWD" ]]; then - _cmux_rename_workspace "$CMUX_WORKSPACE_ID" "$workspace_name" - _cmux_sidebar_refresh "$resolved" - eval "$cc_cmd" - return - fi - - # env -u descarta el socket heredado: ccb opera sobre el dir/workspace destino que el - # usuario indicó explícitamente (no sobre el focused), por eso solo env -u, sin --no-caller - local existing_id - existing_id=$(env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux list-workspaces 2>/dev/null | jq -r --arg name "$workspace_name" '.[] | select(.title == $name) | .id' 2>/dev/null | head -1) - - if [[ -n "$existing_id" ]]; then - _cmux_rename_workspace "$existing_id" "$workspace_name" - env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux select-workspace --workspace "$existing_id" - _cmux_sidebar_refresh "$resolved" - else - if ! env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux new-workspace --name "$workspace_name" --cwd "$resolved" --command "$cc_cmd"; then - echo "⚠️ cmux new-workspace falló, ejecutando Claude Code en el directorio actual" - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$cc_cmd" - fi - fi - elif [[ -n "$TMUX" ]]; then - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$cc_cmd" - else - cd "$resolved" && eval "$cc_cmd" - fi + resolved=$(_resolve_dir_or_fail "${1:-.}") || return 1 + cd "$resolved" && claude --dangerously-skip-permissions } -# Abrir OpenCode con bypass de permisos explícito +# Abrir OpenCode con flags por defecto. ocb() { - local target="${1:-.}" - local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" - return 1 - fi - - local oc_cmd="opencode ${OPENCODE_DEFAULT_FLAGS:-}" - - if _zellij_preferred && _zellij_available; then - _zellij_open_agent "$resolved" "$oc_cmd" "opencode" - elif _zellij_preferred; then - echo "⚠️ zellij no está instalado; ejecutando OpenCode directo" - cd "$resolved" && eval "$oc_cmd" - elif [[ -n "$CMUX_WORKSPACE_ID" ]]; then - local workspace_name - workspace_name="$(_workspace_name_for_path "$resolved")" - - if [[ "$resolved" == "$PWD" ]]; then - _cmux_rename_workspace "$CMUX_WORKSPACE_ID" "$workspace_name" - _cmux_sidebar_refresh "$resolved" - eval "$oc_cmd" - return - fi - - # env -u descarta el socket heredado: ocb opera sobre el dir/workspace destino que el - # usuario indicó explícitamente (no sobre el focused), por eso solo env -u, sin --no-caller - local existing_id - existing_id=$(env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux list-workspaces 2>/dev/null | jq -r --arg name "$workspace_name" '.[] | select(.title == $name) | .id' 2>/dev/null | head -1) - - if [[ -n "$existing_id" ]]; then - _cmux_rename_workspace "$existing_id" "$workspace_name" - env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux select-workspace --workspace "$existing_id" - _cmux_sidebar_refresh "$resolved" - else - if ! env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux new-workspace --name "$workspace_name" --cwd "$resolved" --command "$oc_cmd"; then - echo "⚠️ cmux new-workspace falló, ejecutando OpenCode en el directorio actual" - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$oc_cmd" - fi - fi - elif [[ -n "$TMUX" ]]; then - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$oc_cmd" - else - cd "$resolved" && eval "$oc_cmd" - fi + oc "${1:-.}" } -# Abrir Claude Code con contexto inicial en tmux +# Abrir Claude Code con contexto inicial. ccx() { local context="$1" local target="${2:-.}" @@ -491,55 +63,16 @@ ccx() { fi local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" - return 1 - fi - - if _zellij_preferred && _zellij_available; then - local ctxfile="$HOME/.claude/ccx-ctx-$$.txt" - echo "$context" > "$ctxfile" - chmod 600 "$ctxfile" - _zellij_open_agent "$resolved" "sh -c 'claude < $ctxfile; rm -f $ctxfile'" - elif _zellij_preferred; then - echo "⚠️ zellij no está instalado; ejecutando Claude Code directo" - cd "$resolved" && echo "$context" | claude - elif [[ -n "$CMUX_WORKSPACE_ID" ]]; then - # Estamos dentro de cmux: escribir contexto a tempfile y abrir workspace propio - local workspace_name - workspace_name="$(_workspace_name_for_path "$resolved")" - - # Archivo de contexto en ~/.claude/ para garantizar accesibilidad desde el workspace nuevo - local ctxfile="$HOME/.claude/ccx-ctx-$$.txt" - echo "$context" > "$ctxfile" - chmod 600 "$ctxfile" - - # Crear workspace nuevo — el cleanup va dentro del comando para que - # el archivo siga existiendo cuando cmux lo lea en el workspace nuevo - # env -u descarta el socket heredado: ccx opera sobre el workspace destino explícito - if env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux new-workspace --name "$workspace_name" --cwd "$resolved" --command "sh -c 'claude < $ctxfile; rm -f $ctxfile'"; then - : # limpieza la hace el comando en el workspace nuevo - else - # cmux falló: limpiar archivo y ejecutar claude directo con el contexto - rm -f "$ctxfile" - echo "⚠️ cmux new-workspace falló, ejecutando claude en el directorio actual" - cd "$resolved" && echo "$context" | claude - fi - elif [[ -n "$TMUX" ]]; then - cd "$resolved" && echo "$context" | claude - else - cd "$resolved" && echo "$context" | claude - fi + resolved=$(_resolve_dir_or_fail "$target") || return 1 + cd "$resolved" && echo "$context" | claude } -# Navegar al Claude workspace +# Navegar al workspace principal. ccd() { local subpath="${1:-}" local workspace="${WORKSPACE_DIR:-$HOME/dev}" - local target + if [[ -n "$subpath" ]]; then target="$workspace/$subpath" else @@ -548,14 +81,14 @@ ccd() { if [[ -d "$target" ]]; then cd "$target" - echo "📂 Navegando a: $target" + echo "Navegando a: $target" else - echo "❌ Directorio no encontrado: $target" + echo "Directorio no encontrado: $target" return 1 fi } -# Copiar contexto de código al clipboard para Claude +# Copiar contexto de código al clipboard para Claude. ccclip() { if [[ $# -eq 0 ]]; then echo "Uso: ccclip [archivo2 ...] [-n|--line-numbers]" @@ -573,32 +106,44 @@ ccclip() { done local context="" + local file ext n line for file in "${files[@]}"; do if [[ ! -f "$file" ]]; then - echo "⚠️ Archivo no encontrado: $file" + echo "Archivo no encontrado: $file" continue fi - local ext="${file##*.}" + ext="${file##*.}" context+="\`\`\`$ext\n" context+="// File: $file\n" if $with_numbers; then - local n=1 + n=1 while IFS= read -r line; do context+=$(printf "%4d: %s\n" "$n" "$line") (( n++ )) done < "$file" else - context+="$(cat "$file")\n" + context+="$(<"$file")\n" fi context+="\`\`\`\n\n" done - echo -e "$context" | pbcopy - echo "✓ Contexto copiado al clipboard (${#files[@]} archivo$([ ${#files[@]} -ne 1 ] && echo 's'))" + if command -v pbcopy >/dev/null 2>&1; then + print -r -- "$context" | pbcopy + elif command -v wl-copy >/dev/null 2>&1; then + print -r -- "$context" | wl-copy + elif command -v xclip >/dev/null 2>&1; then + print -r -- "$context" | xclip -selection clipboard + else + print -r -- "$context" + echo "Clipboard no disponible; imprimí el contexto en stdout" + return 1 + fi + + echo "Contexto copiado al clipboard (${#files[@]} archivo$([ ${#files[@]} -ne 1 ] && echo 's'))" } #endregion diff --git a/zsh/scripts/herdr-helpers.zsh b/zsh/scripts/herdr-helpers.zsh new file mode 100644 index 0000000..87ab383 --- /dev/null +++ b/zsh/scripts/herdr-helpers.zsh @@ -0,0 +1,94 @@ +# herdr-helpers.zsh — Remote-first Herdr helpers. + +_herdr_context_label() { + local host + host="${HOST%%.*}" + host="${host:-$(hostname -s 2>/dev/null || hostname)}" + + if [[ -n "$SSH_CONNECTION" || -n "$SSH_CLIENT" || -n "$SSH_TTY" ]]; then + printf 'ssh-%s' "$host" + else + printf 'local-%s' "$host" + fi +} + +_herdr_workspace_name_for_path() { + local target="${1:-$PWD}" + local git_root repo_name parent_name branch + git_root=$(git -C "$target" rev-parse --show-toplevel 2>/dev/null || true) + + if [[ -n "$git_root" ]]; then + repo_name=$(basename "$git_root" | tr '.' '-') + parent_name=$(basename "$(dirname "$git_root")" | tr '.' '-') + branch=$(git -C "$git_root" branch --show-current 2>/dev/null || true) + if [[ -n "$branch" ]]; then + printf '%s-%s-%s' "$parent_name" "$repo_name" "${branch//[^A-Za-z0-9_.-]/-}" + else + printf '%s-%s' "$parent_name" "$repo_name" + fi + else + basename "$target" | tr '.' '-' + fi +} + +_herdr_session_name_for_path() { + printf '%s-%s' "$(_herdr_context_label)" "$(_herdr_workspace_name_for_path "${1:-$PWD}")" +} + +_herdr_current_pane_id() { + command -v herdr >/dev/null 2>&1 || return 1 + herdr pane current --current 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["pane"]["pane_id"])' 2>/dev/null +} + +# Entrar/crear una sesión Herdr nombrada por host + repo + branch del path actual. +hhere() { + local target="${1:-$PWD}" + local resolved session + resolved=$(cd -q "$target" >/dev/null 2>&1 && pwd) || { + echo "Directorio no encontrado: $target" + return 1 + } + session="$(_herdr_session_name_for_path "$resolved")" + CORTEX_MULTIPLEXER=herdr herdr --session "$session" +} + +# Attach remoto con Herdr. Uso: hremote [session] +hremote() { + local target="${1:?Uso: hremote [session]}" + local session="${2:-main}" + CORTEX_MULTIPLEXER=herdr CORTEX_SSH_TARGET="$target" herdr --remote "$target" --session "$session" +} + +# Renombrar el pane actual de Herdr con un label humano o uno derivado de repo/branch. +hname() { + local label="${1:-$(_herdr_workspace_name_for_path "$PWD")}" + local pane_id + pane_id="$(_herdr_current_pane_id)" || { + echo "No pude detectar el pane actual de Herdr" + return 1 + } + herdr pane rename "$pane_id" "$label" >/dev/null && printf 'pane: %s\n' "$label" +} + +# Mostrar contexto completo del shell/pane actual. +herdr-orient() { + printf 'host: %s\n' "$(hostname -s 2>/dev/null || hostname)" + printf 'cwd: %s\n' "$PWD" + if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + printf 'repo: %s\n' "$(git rev-parse --show-toplevel)" + printf 'branch: %s\n' "$(git branch --show-current 2>/dev/null || printf detached)" + fi + [[ -n "$SSH_CONNECTION" ]] && printf 'ssh: %s\n' "${CORTEX_SSH_TARGET:-remote}" + + local pane_id + pane_id="$(_herdr_current_pane_id)" && printf 'herdr: %s\n' "$pane_id" +} + +# Mantener el comando muscular, pero con contexto Herdr incluido cuando existe. +whereami() { + herdr-orient +} + +alias h='herdr' +alias hs='herdr status' +alias hl='herdr workspace list' diff --git a/zsh/scripts/ssh-helpers.zsh b/zsh/scripts/ssh-helpers.zsh index 484435c..c7d2dd7 100644 --- a/zsh/scripts/ssh-helpers.zsh +++ b/zsh/scripts/ssh-helpers.zsh @@ -1,6 +1,6 @@ # ssh-helpers.zsh — Remote helpers with visible host/repo context. -_remote_zellij_command_for_path() { +_remote_herdr_command_for_path() { local remote_path="$1" local shell_path @@ -10,16 +10,16 @@ _remote_zellij_command_for_path() { shell_path="${(q)remote_path}" fi - printf 'cd %s && root=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && repo=$(basename "$root" | tr . -) && parent=$(basename "$(dirname "$root")" | tr . -) && session="$parent-$repo" && zellij attach --create "$session"' "$shell_path" + printf 'cd %s && exec ${SHELL:-zsh}' "$shell_path" } -# Entrar a una workstation remota por Mosh. Si pasás path, attach/crea Zellij remoto por repo. +# Entrar a una workstation remota por Mosh. Si pasás path, entra directo a ese directorio. moshx() { local target="${1:?Uso: moshx [remote-path]}" shift if ! command -v mosh >/dev/null 2>&1; then - echo "❌ mosh no está instalado localmente; fallback: sshx $target ${*}" + echo "mosh no está instalado localmente; fallback: sshx $target ${*}" sshx "$target" "$@" return fi @@ -32,22 +32,23 @@ moshx() { shift local remote_command - remote_command="$(_remote_zellij_command_for_path "$remote_path")" + remote_command="$(_remote_herdr_command_for_path "$remote_path")" CORTEX_SSH_TARGET="$target" mosh "$target" -- sh -lc "$remote_command" || { - echo "⚠️ mosh falló; probando fallback SSH al mismo Zellij remoto" + echo "mosh falló; probando fallback SSH al mismo directorio remoto" CORTEX_SSH_TARGET="$target" ssh -t "$target" sh -lc "$remote_command" } } -# Diagnosticar dependencias remotas para moshx sin abrir sesión interactiva. +# Diagnosticar dependencias remotas para moshx/herdr sin abrir sesión interactiva. moshx-doctor() { local target="${1:?Uso: moshx-doctor }" echo "Local:" command -v mosh >/dev/null 2>&1 && echo " ✓ mosh: $(command -v mosh)" || echo " ✗ mosh local no encontrado" + command -v herdr >/dev/null 2>&1 && echo " ✓ herdr: $(command -v herdr)" || echo " ✗ herdr local no encontrado" echo "Remote $target:" - ssh "$target" 'for cmd in mosh-server zellij git sh; do if command -v "$cmd" >/dev/null 2>&1; then printf " ✓ %s: %s\n" "$cmd" "$(command -v "$cmd")"; else printf " ✗ %s no encontrado\n" "$cmd"; fi; done' + ssh "$target" 'for cmd in mosh-server herdr git sh; do if command -v "$cmd" >/dev/null 2>&1; then printf " ✓ %s: %s\n" "$cmd" "$(command -v "$cmd")"; else printf " ✗ %s no encontrado\n" "$cmd"; fi; done' } # SSH directo, pero exportando CORTEX_SSH_TARGET para que el prompt muestre el host remoto. @@ -57,56 +58,7 @@ sshc() { CORTEX_SSH_TARGET="$target" ssh "$target" "$@" } -# Abrir SSH en una sesión Zellij local nombrada ssh-. Fallback para hosts sin Mosh. +# SSH directo con contexto visible. Herdr se encarga de la persistencia fuera de SSH. sshx() { - local target="${1:?Uso: sshx [ssh args...]}" - shift - - if ! command -v zellij >/dev/null 2>&1; then - sshc "$target" "$@" - return - fi - - local session - session="ssh-${target//[^A-Za-z0-9_.-]/-}" - local quoted_args=("${(@q)@}") - local command_line="CORTEX_SSH_TARGET=${(q)target} ssh ${(q)target} ${quoted_args[*]}" - - if ! typeset -f _zellij_layout_for_command >/dev/null 2>&1 || ! typeset -f _zellij_session_exists >/dev/null 2>&1; then - CORTEX_SSH_TARGET="$target" zellij --session "$session" - return - fi - - if [[ -n "$ZELLIJ" ]]; then - if _zellij_session_exists "$session"; then - zellij action switch-session "$session" - return - fi - - local layout_file - layout_file="$(_zellij_layout_for_command "$PWD" "$command_line" "$session")" - zellij action switch-session --layout "$layout_file" "$session" - else - if _zellij_session_exists "$session"; then - zellij attach "$session" - return - fi - - local layout_file - layout_file="$(_zellij_layout_for_command "$PWD" "$command_line" "$session")" - zellij --session "$session" --layout "$layout_file" - fi -} - -# Mostrar contexto rápido del shell actual: host, cwd, git, zellij/tmux y ssh. -whereami() { - printf 'host: %s\n' "$(hostname -s 2>/dev/null || hostname)" - printf 'cwd: %s\n' "$PWD" - if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - printf 'repo: %s\n' "$(git rev-parse --show-toplevel)" - printf 'branch: %s\n' "$(git branch --show-current 2>/dev/null || printf detached)" - fi - [[ -n "$ZELLIJ" ]] && printf 'zellij: %s\n' "${ZELLIJ_SESSION_NAME:-unknown}" - [[ -n "$TMUX" ]] && printf 'tmux: %s\n' "$(tmux display-message -p '#S:#W.#P' 2>/dev/null || printf unknown)" - [[ -n "$SSH_CONNECTION" ]] && printf 'ssh: %s\n' "${CORTEX_SSH_TARGET:-remote}" + sshc "$@" } diff --git a/zsh/scripts/tmux-helpers.zsh b/zsh/scripts/tmux-helpers.zsh deleted file mode 100644 index a3cd4c0..0000000 --- a/zsh/scripts/tmux-helpers.zsh +++ /dev/null @@ -1,60 +0,0 @@ -# tmux-helpers.zsh — Compat aliases sobre Zellij - -# Alias base: mantenemos la memoria muscular `t*`, pero el backend default es Zellij. -alias t="zellij" - -# Listar sesiones activas -tl() { - zellij list-sessions 2>/dev/null || echo "No hay sesiones Zellij activas" -} - -# Attach a una sesión (o crearla si no existe) -ta() { - local session="${1:-main}" - local layout_file - session="$(_zellij_context_label):$session" - layout_file="$(_zellij_default_layout)" - if _zellij_session_exists "$session"; then - zellij attach "$session" - elif [[ -n "$layout_file" ]]; then - zellij --session "$session" --layout "$layout_file" - else - zellij attach "$session" --create - fi -} - -# Nueva sesión con nombre -tn() { - local session="${1:?Uso: tn }" - local layout_file - session="$(_zellij_context_label):$session" - layout_file="$(_zellij_default_layout)" - if [[ -n "$layout_file" ]]; then - zellij --session "$session" --layout "$layout_file" - else - zellij attach "$session" --create - fi -} - -# Matar una sesión -tk() { - local session="${1:?Uso: tk }" - [[ "$session" == *:* ]] || session="$(_zellij_context_label):$session" - zellij delete-session "$session" --force && echo "✓ Sesión '$session' terminada" -} - -# Sesión de desarrollo: nombre = basename del directorio actual -# Uso: cd ~/dev/work/myproject && tdev -tdev() { - local session - session="$(basename "$PWD" | tr '.' '-')" - ta "$session" -} - -# Sesión de Claude Code en Zellij -# Uso: cd ~/dev/work/myproject && tcc -tcc() { - local session - session="$(basename "$PWD" | tr '.' '-')" - CORTEX_MULTIPLEXER=zellij cc "$PWD" -} diff --git a/zsh/scripts/worktree-helpers.zsh b/zsh/scripts/worktree-helpers.zsh index 57d0e75..6b2610e 100644 --- a/zsh/scripts/worktree-helpers.zsh +++ b/zsh/scripts/worktree-helpers.zsh @@ -1,5 +1,5 @@ #region Worktree Helpers -# Funciones para gestión de git worktrees con integración Zellij. +# Funciones para gestión de git worktrees con integración Herdr. # Detecta repos PCSoft automáticamente y bloquea la creación de worktrees en ellos. # Verifica si el repo actual contiene archivos PCSoft (Categoría B — prohibido worktree) @@ -32,7 +32,7 @@ _wt_path_for() { printf '%s/%s/%s' "$(_wt_base_dir)" "$repo_name" "$name" } -# Crea un worktree en ~/dev/worktrees// y abre sesión Zellij automáticamente. +# Crea un worktree en ~/dev/worktrees// y abre el agente desde Herdr. # Uso: wtadd [branch] # — nombre del worktree (crea ~/dev/worktrees//) # [branch] — branch existente o nueva (default: crea branch nueva con el mismo nombre) @@ -82,9 +82,9 @@ wtadd() { echo "✓ Worktree creado: $wt_path" - # Abrir el agente en el multiplexor default si está disponible. - if [[ "${CORTEX_MULTIPLEXER:-zellij}" == "zellij" ]] || [[ -n "$ZELLIJ" ]]; then - echo "→ Abriendo sesión Zellij..." + # Abrir el agente en el flujo Herdr si está disponible. + if [[ "${CORTEX_MULTIPLEXER:-herdr}" == "herdr" ]]; then + echo "→ Abriendo agente para el worktree..." cc "$wt_path" fi } diff --git a/zsh/zshrc b/zsh/zshrc index b20026a..d345f97 100644 --- a/zsh/zshrc +++ b/zsh/zshrc @@ -56,7 +56,7 @@ export VISUAL="$EDITOR" # Workspace principal de desarrollo export WORKSPACE_DIR="${WORKSPACE_DIR:-$HOME/dev}" export CLAUDE_CODE_EFFORT_LEVEL=high -export CORTEX_MULTIPLEXER="${CORTEX_MULTIPLEXER:-zellij}" +export CORTEX_MULTIPLEXER="${CORTEX_MULTIPLEXER:-herdr}" # Proyectos de la organización export WORK_PROJECTS_DIR="${WORK_PROJECTS_DIR:-$HOME/dev/work}" @@ -221,7 +221,7 @@ if [[ -d "$_SCRIPTS_DIR" ]]; then [[ -f "$_SCRIPTS_DIR/claude-helpers.zsh" ]] && source "$_SCRIPTS_DIR/claude-helpers.zsh" [[ -f "$_SCRIPTS_DIR/git-helpers.zsh" ]] && source "$_SCRIPTS_DIR/git-helpers.zsh" [[ -f "$_SCRIPTS_DIR/ssh-helpers.zsh" ]] && source "$_SCRIPTS_DIR/ssh-helpers.zsh" - [[ -f "$_SCRIPTS_DIR/tmux-helpers.zsh" ]] && source "$_SCRIPTS_DIR/tmux-helpers.zsh" + [[ -f "$_SCRIPTS_DIR/herdr-helpers.zsh" ]] && source "$_SCRIPTS_DIR/herdr-helpers.zsh" [[ -f "$_SCRIPTS_DIR/worktree-helpers.zsh" ]] && source "$_SCRIPTS_DIR/worktree-helpers.zsh" fi #endregion @@ -239,6 +239,37 @@ if command -v atuin &>/dev/null; then fi #endregion +#region Terminal Title +_cortex_terminal_title() { + [[ -t 1 ]] || return 0 + + local host context repo dir title + host="${HOST%%.*}" + host="${host:-$(hostname -s 2>/dev/null || hostname)}" + + if [[ -n "$SSH_CONNECTION" || -n "$SSH_CLIENT" || -n "$SSH_TTY" ]]; then + context="ssh:$host" + else + context="$host" + fi + + repo=$(git rev-parse --show-toplevel 2>/dev/null) + if [[ -n "$repo" ]]; then + dir="$(basename "$repo")" + else + dir="${PWD/#$HOME/~}" + fi + + title="herdr | $context | $dir" + + print -Pn "\e]0;${title}\a" +} + +autoload -Uz add-zsh-hook +add-zsh-hook precmd _cortex_terminal_title +add-zsh-hook chpwd _cortex_terminal_title +#endregion + #region Starship if command -v starship &>/dev/null; then eval "$(starship init zsh)" @@ -297,39 +328,22 @@ help-profile() { echo " ${C}.., ..., .... ${R}Subir 1, 2 o 3 niveles" echo "\n${T}Claude Code${R}" - echo " ${C}cc [path] ${R}Abrir Claude Code en directorio" - echo " ${C}oc [path] ${R}Abrir OpenCode en directorio" - echo " ${C}zj [path] ${R}Entrar/crear sesión Zellij por repo/path" - echo " ${C}zsessions ${R}Listar sesiones Zellij" + echo " ${C}cc [path] ${R}Abrir Claude Code en directorio actual/pasado" + echo " ${C}oc [path] ${R}Abrir OpenCode en directorio actual/pasado" echo " ${C}ccx [p] ${R}Claude Code con contexto inicial" echo " ${C}ccd [sub] ${R}Navegar al Claude workspace" echo " ${C}ccclip ${R}Copiar código al clipboard" - echo "\n${T}Zellij — sesiones${R}" - echo " ${C}zj [path] ${R}Entrar/crear sesión Zellij por repo/path" - echo " ${C}zsessions ${R}Listar sesiones Zellij" - echo " ${C}tcc ${R}Abrir Claude Code en sesión Zellij del repo" - echo "\n${T}SSH / ubicación${R}" - echo " ${C}moshx [path] ${R}Mosh a host; con path entra al Zellij remoto del repo" - echo " ${C}moshx-doctor ${R}Verificar mosh-server, zellij, git y sh en el host" - echo " ${C}sshx ${R}SSH en sesión Zellij ssh-" + echo " ${C}hhere [path] ${R}Entrar/crear sesión Herdr por host+repo+branch" + echo " ${C}hremote [s] ${R}Attach Herdr remoto a host con sesión opcional" + echo " ${C}hname [label] ${R}Nombrar pane Herdr actual" + echo " ${C}hs, hl ${R}Status Herdr y workspaces" + echo " ${C}moshx [path] ${R}Mosh/SSH resiliente al host; Herdr es el multiplexor" + echo " ${C}moshx-doctor ${R}Verificar mosh-server, herdr, git y sh en el host" + echo " ${C}sshx ${R}SSH directo con contexto visible" echo " ${C}sshc ${R}SSH directo con host visible en prompt" echo " ${C}whereami ${R}Mostrar host, cwd, repo, sesión y SSH" - - echo "\n${T}Tmux — sesiones${R}" - echo " ${C}tcc ${R}Abrir Claude Code + lazygit (layout automático)" - echo " ${C}tdev ${R}Sesión con nombre del directorio actual" - echo " ${C}ta [nombre] ${R}Attach a sesión (o crearla)" - echo " ${C}tn ${R}Nueva sesión con nombre" - echo " ${C}tl ${R}Listar sesiones activas" - echo " ${C}tk ${R}Matar sesión" - - echo "\n${T}Zellij — keybindings${R}" - echo " ${C}Ctrl+P ${R}Modo pane" - echo " ${C}Ctrl+T ${R}Modo tab" - echo " ${C}Ctrl+S ${R}Modo resize" - echo " ${C}Ctrl+G ${R}Bloquear/desbloquear keybindings" echo "" } From 873cb1f920a1079ce5acab93271014e4376c6e1e Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Tue, 30 Jun 2026 14:57:48 +0000 Subject: [PATCH 6/6] feat: improve Herdr session helpers UX --- README.md | 15 ++++- zsh/scripts/herdr-helpers.zsh | 104 +++++++++++++++++++++++++++++++--- zsh/zshrc | 6 +- 3 files changed, 114 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b34eea9..8b47f27 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,10 @@ dotfiles/ | `cortex`, `dotfiles` | Navegación rápida al repo `cortex` y sus dotfiles | | `cc [path]` | Abrir Claude Code | | `oc [path]` | Abrir OpenCode | -| `hhere [path]` | Entrar/crear sesión Herdr por host+repo+branch | +| `hhere`, `hmain` | Volver a la sesión Herdr principal del repo/branch | +| `hnew [path]` | Crear sesión Herdr independiente con timestamp | +| `hrole [path]` | Entrar/crear sesión Herdr por rol operativo | +| `hfocus`, `hside`, `hscratch` | Sesiones Herdr por rol para foco, lateral o scratch | | `hremote [session]` | Attach remoto con `herdr --remote` | | `hname [label]` | Nombrar el pane Herdr actual | | `moshx [remote-path]` | Mosh al host; con path entra a ese directorio remoto | @@ -112,11 +115,13 @@ Editá `local/env.zsh` (gitignored) para configurar: ## Herdr remoto -Para SSH/remoto, el modelo recomendado es Herdr. Usá un workspace por repo, tabs por objetivo y panes por agente/proceso: +Para SSH/remoto, el modelo recomendado es Herdr. Usá sesiones nombradas para separar tableros persistentes, workspaces por repo, tabs por objetivo y panes por agente/proceso: ```bash hremote agent-dev-01 main hhere ~/dev/personal/cortex +hfocus ~/dev/personal/cortex +hside ~/dev/personal/cortex ``` Si necesitás diagnosticar dependencias: @@ -127,10 +132,14 @@ moshx-doctor agent-dev-01 Comportamiento: -- `hhere [path]` nombra la sesión por host + repo + branch. +- `hhere [path]` y `hmain [path]` nombran la sesión principal por host + repo + branch, para reattach exacto. +- `hnew [path]` crea otra sesión independiente del mismo repo/branch con timestamp corto. +- `hrole [path]`, `hfocus [path]`, `hside [path]` y `hscratch [path]` crean/entran a sesiones independientes por intención operativa, no por monitor físico. +- Si ya estás dentro de Herdr, esos helpers no intentan abrir Herdr anidado: crean o enfocan un workspace con el mismo nombre dentro de la sesión actual. - `hremote [session]` usa el bridge remoto de Herdr. - `hname [label]` evita panes anónimos en el sidepanel. - `cc [path]`, `ccb [path]`, `oc [path]` y `ocb [path]` ejecutan el agente en el pane actual; Herdr provee persistencia. +- Herdr también expone CLI scriptable para `workspace`, `tab`, `pane`, `agent`, `worktree`, `wait` e `integration`; los helpers solo cubren el flujo muscular diario. - Prompt Starship marca `herdr` cuando `CORTEX_MULTIPLEXER=herdr`. - `whereami` muestra ubicación completa sin depender de la UI. diff --git a/zsh/scripts/herdr-helpers.zsh b/zsh/scripts/herdr-helpers.zsh index 87ab383..9c8a6ba 100644 --- a/zsh/scripts/herdr-helpers.zsh +++ b/zsh/scripts/herdr-helpers.zsh @@ -35,23 +35,113 @@ _herdr_session_name_for_path() { printf '%s-%s' "$(_herdr_context_label)" "$(_herdr_workspace_name_for_path "${1:-$PWD}")" } -_herdr_current_pane_id() { - command -v herdr >/dev/null 2>&1 || return 1 - herdr pane current --current 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["pane"]["pane_id"])' 2>/dev/null +_herdr_session_name_with_suffix() { + local session_path="${1:-$PWD}" + local suffix="$2" + + if [[ -n "$suffix" ]]; then + printf '%s-%s' "$(_herdr_session_name_for_path "$session_path")" "${suffix//[^A-Za-z0-9_.-]/-}" + else + _herdr_session_name_for_path "$session_path" + fi } -# Entrar/crear una sesión Herdr nombrada por host + repo + branch del path actual. -hhere() { +_herdr_open_session_for_path() { local target="${1:-$PWD}" - local resolved session + local suffix="${2:-}" + local resolved session workspace_label + resolved=$(cd -q "$target" >/dev/null 2>&1 && pwd) || { echo "Directorio no encontrado: $target" return 1 } - session="$(_herdr_session_name_for_path "$resolved")" + + if [[ "${HERDR_ENV:-}" == "1" ]]; then + workspace_label="$(_herdr_session_name_with_suffix "$resolved" "$suffix")" + workspace_label="${workspace_label#$(_herdr_context_label)-}" + _herdr_focus_or_create_workspace "$workspace_label" "$resolved" + return + fi + + session="$(_herdr_session_name_with_suffix "$resolved" "$suffix")" CORTEX_MULTIPLEXER=herdr herdr --session "$session" } +_herdr_workspace_id_by_label() { + local label="$1" + herdr workspace list 2>/dev/null | python3 -c ' +import json +import sys + +label = sys.argv[1] +try: + data = json.load(sys.stdin) +except Exception: + sys.exit(0) + +for workspace in data.get("result", {}).get("workspaces", []): + if workspace.get("label") == label: + print(workspace.get("workspace_id", "")) + break +' "$label" 2>/dev/null +} + +_herdr_focus_or_create_workspace() { + local label="$1" + local cwd="$2" + local workspace_id + + workspace_id="$(_herdr_workspace_id_by_label "$label")" + if [[ -n "$workspace_id" ]]; then + herdr workspace focus "$workspace_id" >/dev/null && printf 'workspace: %s\n' "$label" + else + herdr workspace create --cwd "$cwd" --label "$label" --focus >/dev/null && printf 'workspace: %s\n' "$label" + fi +} + +_herdr_current_pane_id() { + command -v herdr >/dev/null 2>&1 || return 1 + herdr pane current --current 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["pane"]["pane_id"])' 2>/dev/null +} + +# Entrar/crear una sesión Herdr nombrada por host + repo + branch del path actual. +hhere() { + _herdr_open_session_for_path "${1:-$PWD}" +} + +# Alias semántico de hhere: volver a la sesión principal del repo/branch actual. +hmain() { + hhere "${1:-$PWD}" +} + +# Entrar/crear una sesión Herdr con rol humano. Uso: hrole [path] +hrole() { + local role="${1:?Uso: hrole [path]}" + local target="${2:-$PWD}" + _herdr_open_session_for_path "$target" "$role" +} + +# Entrar/crear una sesión Herdr independiente con timestamp corto. +hnew() { + local target="${1:-$PWD}" + _herdr_open_session_for_path "$target" "new-$(date +%H%M%S)-$RANDOM" +} + +# Entrar/crear la sesión Herdr de trabajo principal/intenso para este repo/branch. +hfocus() { + _herdr_open_session_for_path "${1:-$PWD}" "focus" +} + +# Entrar/crear una sesión Herdr lateral para este repo/branch. +hside() { + _herdr_open_session_for_path "${1:-$PWD}" "side" +} + +# Entrar/crear una sesión Herdr temporal para pruebas o tareas descartables. +hscratch() { + _herdr_open_session_for_path "${1:-$PWD}" "scratch" +} + # Attach remoto con Herdr. Uso: hremote [session] hremote() { local target="${1:?Uso: hremote [session]}" diff --git a/zsh/zshrc b/zsh/zshrc index d345f97..206ee7a 100644 --- a/zsh/zshrc +++ b/zsh/zshrc @@ -335,7 +335,11 @@ help-profile() { echo " ${C}ccclip ${R}Copiar código al clipboard" echo "\n${T}SSH / ubicación${R}" - echo " ${C}hhere [path] ${R}Entrar/crear sesión Herdr por host+repo+branch" + echo " ${C}hhere/hmain [p] ${R}Volver a la sesión Herdr principal del repo/branch" + echo " ${C}hnew [path] ${R}Crear sesión Herdr independiente con timestamp" + echo " ${C}hrole [p] ${R}Entrar/crear sesión Herdr por rol operativo" + echo " ${C}hfocus/hside ${R}Sesiones Herdr por rol: foco o lateral" + echo " ${C}hscratch [path] ${R}Sesión Herdr temporal para pruebas" echo " ${C}hremote [s] ${R}Attach Herdr remoto a host con sesión opcional" echo " ${C}hname [label] ${R}Nombrar pane Herdr actual" echo " ${C}hs, hl ${R}Status Herdr y workspaces"