diff --git a/README.md b/README.md index 497d64c78ca..7312399458b 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,80 @@ defaults, and so on. Tweak this script, and occasionally run `dot` from time to time to keep your environment fresh and up-to-date. You can find this script in `bin/`. +## tmux-sessionizer + +`tmux-sessionizer` is a project-picker that lets you jump into any project as a +dedicated tmux session with one keystroke. When tmuxinator is installed it +automatically opens three windows — **nvim** at the project root, and two +plain **terminal** windows — so you land in a ready-to-code environment. + +### Prerequisites + +| Tool | Required? | Install | +|------|-----------|---------| +| [tmux](https://github.com/tmux/tmux) | **yes** | `brew install tmux` | +| [fzf](https://github.com/junegunn/fzf) | **yes** | `brew install fzf` | +| [fd](https://github.com/sharkdp/fd) | recommended | `brew install fd` | +| [tmuxinator](https://github.com/tmuxinator/tmuxinator) | optional (richer layout) | `brew install tmuxinator` | +| [Neovim](https://neovim.io) | optional (used in window 1) | `brew install neovim` | + +### Project directories + +By default `tmux-sessionizer` searches **one level deep** inside these two +folders: + +``` +~/dev/ +~/Code/ +``` + +Create either (or both) directories and put your projects inside them: + +```sh +mkdir -p ~/dev +git clone git@github.com:you/my-project.git ~/dev/my-project +``` + +### How to invoke it + +| Where | Keys | What happens | +|-------|------|--------------| +| Inside **tmux** | **Alt + F** | Opens an fzf picker in a floating popup | +| Any **shell prompt** | **Ctrl + G** | Runs the picker inline in your terminal | +| **Command line** | `tmux-sessionizer` | Same as above (it's on your `$PATH`) | +| **Pass a path directly** | `tmux-sessionizer ~/dev/my-project` | Skips the picker and opens that project | + +### What happens when you pick a project + +1. If a tmux session named after the project already exists you are + switched/attached to it immediately — nothing else changes. +2. **With tmuxinator installed** a fresh session is created with three windows: + - `nvim` — starts `nvim .` at the project root + - `terminal` — plain shell at the project root + - `terminal2` — plain shell at the project root +3. **Without tmuxinator** a single-window session is created and `nvim` is + launched automatically. + +### Per-project customisation + +Drop a `.tmux-sessionizer` shell script in the root of any project (or in +`$HOME` for a global default). The script is sourced inside the new session +after it is created, so you can run extra setup steps: + +```sh +# ~/dev/my-project/.tmux-sessionizer +tmux send-keys -t my-project "docker compose up -d" C-m +``` + +### Window layout reference + +``` +Session: my-project + 0: nvim ← nvim . (opened at project root) + 1: terminal ← plain shell + 2: terminal2 ← plain shell +``` + ## bugs I want this to work for everyone; that means when you clone it down it should diff --git a/bin/tmux-sessionizer b/bin/tmux-sessionizer index 55a4dd07e36..a3f19a1f502 100755 --- a/bin/tmux-sessionizer +++ b/bin/tmux-sessionizer @@ -21,39 +21,37 @@ hydrate() { fi } -run_tmuxinator_if_available() { +find_tmuxinator() { + if [ -x "/opt/homebrew/bin/tmuxinator" ]; then + echo "/opt/homebrew/bin/tmuxinator" + elif [ -x "/usr/local/bin/tmuxinator" ]; then + echo "/usr/local/bin/tmuxinator" + elif command -v tmuxinator >/dev/null 2>&1; then + command -v tmuxinator + fi +} + +run_with_tmuxinator() { local selected_path="$1" - local selected_basename - selected_basename="$(basename "$selected_path")" + local selected_name="$2" local dotfiles_root dotfiles_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" - local tmuxinator_project="$dotfiles_root/tmux/simplepractice.yml" + local tmuxinator_template="$dotfiles_root/tmux/template.yml" - if [[ "$selected_basename" != "simplepractice" ]]; then + if [ ! -f "$tmuxinator_template" ]; then return 1 fi - if [ ! -f "$tmuxinator_project" ]; then - return 1 - fi - - local tmuxinator_cmd="" - if [ -x "/opt/homebrew/bin/tmuxinator" ]; then - tmuxinator_cmd="/opt/homebrew/bin/tmuxinator" - elif [ -x "/usr/local/bin/tmuxinator" ]; then - tmuxinator_cmd="/usr/local/bin/tmuxinator" - elif command -v tmuxinator >/dev/null 2>&1; then - tmuxinator_cmd="$(command -v tmuxinator)" - else + local tmuxinator_cmd + tmuxinator_cmd="$(find_tmuxinator)" + if [ -z "$tmuxinator_cmd" ]; then return 1 fi - if "$tmuxinator_cmd" start -p "$tmuxinator_project"; then - return 0 - fi - - return 1 + TMUXINATOR_PROJECT_NAME="$selected_name" \ + TMUXINATOR_PROJECT_ROOT="$selected_path" \ + "$tmuxinator_cmd" start -p "$tmuxinator_template" } pick_directory() { @@ -96,13 +94,21 @@ if [[ -z $selected ]]; then exit 0 fi -if run_tmuxinator_if_available "$selected"; then +selected_name=$(basename "$selected" | tr . _) +tmux_running=$(pgrep tmux) + +# If the session already exists, just switch to it. +if has_session "$selected_name"; then + switch_to "$selected_name" exit 0 fi -selected_name=$(basename "$selected" | tr . _) -tmux_running=$(pgrep tmux) +# New session: try tmuxinator first for a full window layout. +if run_with_tmuxinator "$selected" "$selected_name"; then + exit 0 +fi +# Fallback: create a plain session with nvim open in the first window. if [[ -z $TMUX ]] && [[ -z $tmux_running ]]; then tmux new-session -s "$selected_name" -c "$selected" tmux send-keys -t "$selected_name" "nvim" C-m @@ -110,10 +116,8 @@ if [[ -z $TMUX ]] && [[ -z $tmux_running ]]; then exit 0 fi -if ! has_session "$selected_name"; then - tmux new-session -ds "$selected_name" -c "$selected" - tmux send-keys -t "$selected_name" "nvim" C-m - hydrate "$selected_name" "$selected" -fi +tmux new-session -ds "$selected_name" -c "$selected" +tmux send-keys -t "$selected_name" "nvim" C-m +hydrate "$selected_name" "$selected" switch_to "$selected_name" diff --git a/tmux/template.yml b/tmux/template.yml new file mode 100644 index 00000000000..68810ae29d0 --- /dev/null +++ b/tmux/template.yml @@ -0,0 +1,12 @@ +# Generic tmuxinator template used by tmux-sessionizer. +# The project name and root are injected via environment variables +# TMUXINATOR_PROJECT_NAME and TMUXINATOR_PROJECT_ROOT. +name: <%= ENV.fetch("TMUXINATOR_PROJECT_NAME", "project") %> +root: <%= ENV.fetch("TMUXINATOR_PROJECT_ROOT", "~") %> + +windows: + - nvim: + panes: + - nvim . + - terminal: "" + - terminal2: "" diff --git a/tmux/tmux.conf.symlink b/tmux/tmux.conf.symlink index 30e07bde57f..049c016a819 100644 --- a/tmux/tmux.conf.symlink +++ b/tmux/tmux.conf.symlink @@ -65,3 +65,7 @@ bind-key -n M-0 select-window -t 0 # Clear pane (screen + scrollback) like Cmd+K bind-key -n M-k send-keys -R "clear" \; send-keys Enter \; run-shell "tmux clear-history" + +# Open project picker (tmux-sessionizer) in a floating popup. +# Uses Alt+F to match the existing Alt-based window navigation pattern. +bind-key -n M-f display-popup -E "tmux-sessionizer" diff --git a/zsh/config.zsh b/zsh/config.zsh index 798392f0044..ee78da1c1a6 100644 --- a/zsh/config.zsh +++ b/zsh/config.zsh @@ -38,6 +38,10 @@ bindkey '^[[5C' end-of-line bindkey '^[[3~' delete-char bindkey '^?' backward-delete-char +# Open project picker (tmux-sessionizer) with Ctrl+G from any shell prompt. +# Ctrl+G avoids conflicts with zsh navigation bindings (^F is forward-char). +bindkey -s '^g' 'tmux-sessionizer\n' + function y() { local tmp="$(mktemp -t "yazi-cwd.XXXXXX")" cwd yazi "$@" --cwd-file="$tmp"