Skip to content

Instantly share code, notes, and snippets.

@daniellin-eero
Last active June 1, 2026 00:51
Show Gist options
  • Select an option

  • Save daniellin-eero/f0fcb9b6027aca7793a647bf16fc92ee to your computer and use it in GitHub Desktop.

Select an option

Save daniellin-eero/f0fcb9b6027aca7793a647bf16fc92ee to your computer and use it in GitHub Desktop.
fix lsp nvim
devup() {
git submodule update --init --recursive
local container_id=$(docker ps -aq --filter "label=devcontainer.local_folder=$(pwd)")
if [[ -z "$container_id" ]]; then
# No container exists, create it
devcontainer up --workspace-folder . --skip-post-create
container_id=$(docker ps -aq --filter "label=devcontainer.local_folder=$(pwd)")
elif [[ -z $(docker ps -q --filter "id=$container_id") ]]; then
# Container exists but not running, start it
docker start $container_id
fi
# Execute bash in the container
docker exec -it -w /workspaces/$(basename $(pwd)) $container_id /bin/bash
}
# Clear line and run tmux-sessionizer
tmux-sessionizer-widget() {
BUFFER="tmux-sessionizer"
zle accept-line
}
zle -N tmux-sessionizer-widget
bindkey ^f tmux-sessionizer-widget
# Session 0
tmux-sessionizer-0() {
BUFFER="tmux-sessionizer -s 0"
zle accept-line
}
zle -N tmux-sessionizer-0
bindkey '\ej' tmux-sessionizer-0
# Session 1
tmux-sessionizer-1() {
BUFFER="tmux-sessionizer -s 1"
zle accept-line
}
zle -N tmux-sessionizer-1
bindkey '\ek' tmux-sessionizer-1
# Session 2
tmux-sessionizer-2() {
BUFFER="tmux-sessionizer -s 2"
zle accept-line
}
zle -N tmux-sessionizer-2
bindkey '\el' tmux-sessionizer-2
# Session 3
tmux-sessionizer-3() {
BUFFER="tmux-sessionizer -s 3"
zle accept-line
}
zle -N tmux-sessionizer-3
bindkey '\e;' tmux-sessionizer-3
-- [[ Configure tmux-sessionizer ]]
-- (tmux manager)
require "danielpclin.tmux-sessionizer-setup"
-- [[ Configure LSP ]]
-- This function gets run when an LSP connects to a particular buffer.
local on_attach = function(_, bufnr)
-- NOTE: Remember that lua is a real programming language, and as such it is possible
-- to define small helper and utility functions so you don't have to repeat yourself
-- many times.
--
-- In this case, we create a function that lets us more easily define mappings specific
-- for LSP related items. It sets the mode, buffer and description for us each time.
local nmap = function(keys, func, desc)
if desc then
desc = "LSP: " .. desc
end
vim.keymap.set("n", keys, func, { buffer = bufnr, desc = desc })
end
local imap = function(keys, func, desc)
if desc then
desc = "LSP: " .. desc
end
vim.keymap.set("i", keys, func, { buffer = bufnr, desc = desc })
end
nmap("<leader>rn", vim.lsp.buf.rename, "[R]e[n]ame")
nmap("<leader>ca", vim.lsp.buf.code_action, "[C]ode [A]ction")
nmap("gd", require("telescope.builtin").lsp_definitions, "[G]oto [D]efinition")
nmap("gr", require("telescope.builtin").lsp_references, "[G]oto [R]eferences")
nmap("gi", require("telescope.builtin").lsp_implementations, "[G]oto [I]mplementation")
nmap("<leader>vt", require("telescope.builtin").lsp_type_definitions, "[V]iew [T]ype Definition")
nmap("<leader>sd", require("telescope.builtin").lsp_document_symbols, "[S]earch [D]ocument Symbols")
nmap("<leader>ss", require("telescope.builtin").lsp_dynamic_workspace_symbols, "[S]earch Workspace [S]ymbols")
-- See `:help K` for why this keymap
nmap("K", vim.lsp.buf.hover, "Hover Documentation")
nmap("<C-s>", vim.lsp.buf.signature_help, "Signature Documentation")
imap("<C-q>", vim.lsp.buf.hover, "Hover Documentation")
imap("<C-s>", vim.lsp.buf.signature_help, "Signature Documentation")
-- Lesser used LSP functionality
-- nmap("gD", vim.lsp.buf.declaration, "[G]oto [D]eclaration")
nmap("<leader>wa", vim.lsp.buf.add_workspace_folder, "[W]orkspace [A]dd Folder")
nmap("<leader>wr", vim.lsp.buf.remove_workspace_folder, "[W]orkspace [R]emove Folder")
nmap("<leader>wl", function()
print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
end, "[W]orkspace [L]ist Folders")
-- -- Create a command `:Format` local to the LSP buffer
-- vim.api.nvim_buf_create_user_command(bufnr, 'Format', function(_)
-- vim.lsp.buf.format()
-- end, { desc = 'Format current buffer with LSP' })
end
-- nvim-cmp supports additional completion capabilities, so broadcast that to servers
local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities = require("cmp_nvim_lsp").default_capabilities(capabilities)
vim.lsp.config("*", {
on_attach = on_attach,
capabilities = capabilities,
})
-- Enable the following language servers
-- Feel free to add/remove any LSPs that you want here. They will automatically be installed.
--
-- Add any additional override configuration in the following tables. They will be passed to
-- the `settings` field of the server config. You must look up that documentation yourself.
--
-- If you want to override the default filetypes that your language server will attach to you can
-- define the property 'filetypes' to the map in question.
local servers = {
clangd = {
-- cmd = {
-- "clangd", "--header-insertion=never",
-- },
},
gopls = {},
pyright = {},
rust_analyzer = {},
ts_ls = {},
html = {
filetypes = { "html" },
},
lua_ls = {
on_init = function(client)
if client.workspace_folders then
local path = client.workspace_folders[1].name
if
path ~= vim.fn.stdpath "config"
and (vim.uv.fs_stat(path .. "/.luarc.json") or vim.uv.fs_stat(path .. "/.luarc.jsonc"))
then
return
end
end
client.config.settings.Lua = vim.tbl_deep_extend("force", client.config.settings.Lua, {
runtime = {
-- Tell the language server which version of Lua you're using (most
-- likely LuaJIT in the case of Neovim)
version = "LuaJIT",
-- Tell the language server how to find Lua modules same way as Neovim
-- (see `:h lua-module-load`)
path = {
"lua/?.lua",
"lua/?/init.lua",
},
},
-- Make the server aware of Neovim runtime files
workspace = {
checkThirdParty = false,
library = {
vim.env.VIMRUNTIME,
-- Depending on the usage, you might want to add additional paths
-- here.
-- '${3rd}/luv/library'
-- '${3rd}/busted/library'
},
-- Or pull in all of 'runtimepath'.
-- NOTE: this is a lot slower and will cause issues when working on
-- your own configuration.
-- See https://github.com/neovim/nvim-lspconfig/issues/3189
-- library = {
-- vim.api.nvim_get_runtime_file('', true),
-- }
},
})
end,
settings = {
Lua = {
workspace = { checkThirdParty = false },
telemetry = { enable = false },
},
},
},
dockerls = {},
docker_compose_language_service = {},
}
for server_name, config in pairs(servers) do
-- vim.print(server_name)
-- vim.print(config)
config['on_attach'] = on_attach
config['capabilities'] = capabilities
vim.lsp.config(server_name, config or {})
vim.lsp.enable(server_name)
end
require("mason").setup()
require("mason-lspconfig").setup {
automatic_enable = false, -- HACK: rely on lspconfig[server_name].setup to enable the LSPs. For some reason, pyright doesn't get enabled this way
ensure_installed = vim.tbl_keys(servers),
}
require("mason-tool-installer").setup {
ensure_installed = {
"prettier", -- prettier formatter
"stylua", -- lua formatter
"isort", -- python formatter
"black", -- python formatter
"pylint", -- python linter
"eslint_d", -- js linter
"ansible-lint", --ansible linter
},
}
-- Setup neovim lua configuration
require("neodev").setup()
-- vim: ts=2 sts=2 sw=2 et
#!/usr/bin/env bash
CONFIG_FILE_NAME="tmux-sessionizer.conf"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/tmux-sessionizer"
CONFIG_FILE="$CONFIG_DIR/$CONFIG_FILE_NAME"
PANE_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/tmux-sessionizer"
PANE_CACHE_FILE="$PANE_CACHE_DIR/panes.cache"
# config file example
# ------------------------
# # file: ~/.config/tmux-sessionizer/tmux-sessionizer.conf
# # If set this override the default TS_SEARCH_PATHS (~/personal ~/projects)
# TS_SEARCH_PATHS=(~/)
# # If set this add additional search paths to the default TS_SEARCH_PATHS
# # The number prefix is the depth for the Path [OPTIONAL]
# TS_EXTRA_SEARCH_PATHS=(~/ghq:3 ~/Git:3 ~/.config:2)
# # if set this override the TS_MAX_DEPTH (1)
# TS_MAX_DEPTH=2
# This is not meant to override .tmux-sessionizer. At first i thought this
# would be a good command, but i don't think its ackshually what i want.
#
# Instead, its a list of commands to run on windows who's index is way outside
# of the first 10 windows. This allows you to create as many windows in your
# session as you would like without having your workflow interrupted by these
# programatic windows
#
# how to use:
# tmux-sessionizer -s 0 will execute the first command in window -t 69.
# use single quotes to wrap the command.
# TS_SESSION_COMMANDS=(<cmd1> <cmd2>)
#
# TS_LOG=true # will write logs to ~/.local/share/tmux-sessionizer/tmux-sessionizer.logs
# TS_LOG_FILE=<file> # will write logs to <file> Defaults to ~/.local/share/tmux-sessionizer/tmux-sessionizer.logs
# ------------------------
if [[ -f "$CONFIG_FILE" ]]; then
source "$CONFIG_FILE"
fi
if [[ -f "$CONFIG_FILE_NAME" ]]; then
source "$CONFIG_FILE_NAME"
fi
if [[ $TS_LOG != "true" ]]; then
if [[ -z $TS_LOG_FILE ]]; then
TS_LOG_FILE="$HOME/.local/share/tmux-sessionizer/tmux-sessionizer.logs"
fi
mkdir -p "$(dirname "$TS_LOG_FILE")"
fi
log() {
if [[ -z $TS_LOG ]]; then
return
elif [[ $TS_LOG == "echo" ]]; then
echo "$*"
elif [[ $TS_LOG == "file" ]]; then
echo "$*" >> "$TS_LOG_FILE"
fi
}
# if TS_SEARCH_PATHS is not set use default
[[ -n "$TS_SEARCH_PATHS" ]] || TS_SEARCH_PATHS=(~/personal ~/projects)
# Add any extra search paths to the TS_SEARCH_PATHS array
# e.g : EXTRA_SEARCH_PATHS=("$HOME/extra1:4" "$HOME/extra2")
# note : Path can be suffixed with :number to limit or extend the depth of the search for the Path
if [[ ${#TS_EXTRA_SEARCH_PATHS[@]} -gt 0 ]]; then
TS_SEARCH_PATHS+=("${TS_EXTRA_SEARCH_PATHS[@]}")
fi
# utility function to find directories
find_dirs() {
# list TMUX sessions
if [[ -n "${TMUX}" ]]; then
current_session=$(tmux display-message -p '#S')
tmux list-sessions -F "[TMUX] #{session_name}" 2>/dev/null | grep -vFx "[TMUX] $current_session" | sort -Vr
else
tmux list-sessions -F "[TMUX] #{session_name}" 2>/dev/null | sort -Vr
fi
# note: TS_SEARCH_PATHS is an array of paths to search for directories
# if the path ends with :number, it will search for directories with a max depth of number ;)
# if there is no number, it will search for directories with a max depth defined by TS_MAX_DEPTH or 1 if not set
for entry in "${TS_SEARCH_PATHS[@]}"; do
# Check if entry as :number as suffix then adapt the maxdepth parameter
if [[ "$entry" =~ ^([^:]+):([0-9]+)$ ]]; then
path="${BASH_REMATCH[1]}"
depth="${BASH_REMATCH[2]}"
else
path="$entry"
fi
[[ -d "$path" ]] && find "$path" -mindepth 1 -maxdepth "${depth:-${TS_MAX_DEPTH:-1}}" -path '*/.git' -prune -o -type d -print
done | sort -Vr
}
session_idx=""
session_cmd=""
user_selected=""
split_type=""
VERSION="0.1.0"
while [[ "$#" -gt 0 ]]; do
case "$1" in
-h | --help)
echo "Usage: tmux-sessionizer [OPTIONS] [SEARCH_PATH]"
echo ""
echo "A tmux session manager that creates and switches between project sessions."
echo ""
echo "Options:"
echo " -h, --help Display this help message"
echo " -v, --version Show version information"
echo " -s, --session <index> Execute session command by index (requires TS_SESSION_COMMANDS)"
echo " --vsplit Create vertical split for session command (use with -s)"
echo " --hsplit Create horizontal split for session command (use with -s)"
echo " --find-dirs List all available directories and tmux sessions"
echo ""
echo "Interactive Mode:"
echo " When run without arguments, opens fzf to select from:"
echo " - Existing tmux sessions (prefixed with [TMUX])"
echo " - Project directories from configured search paths"
echo ""
echo " Keybindings in fzf:"
echo " ENTER - Select and switch to session/directory"
echo " CTRL-D - Kill selected tmux session"
echo ""
echo "Configuration:"
echo " Config file: ~/.config/tmux-sessionizer/tmux-sessionizer.conf"
echo " See script comments for configuration options."
echo ""
echo "Examples:"
echo " tmux-sessionizer # Interactive mode"
echo " tmux-sessionizer ~/my-project # Direct path selection"
echo " tmux-sessionizer -s 0 # Execute first session command"
echo " tmux-sessionizer -s 1 --vsplit # Execute second command in vertical split"
exit 0
;;
-s | --session)
session_idx="$2"
if [[ -z $session_idx ]]; then
echo "Session index cannot be empty"
exit 1
fi
if [[ -z $TS_SESSION_COMMANDS ]]; then
echo "TS_SESSION_COMMANDS is not set. Must have a command set to run when switching to a session"
exit 1
fi
if [[ -z "$session_idx" || "$session_idx" -lt 0 || "$session_idx" -ge "${#TS_SESSION_COMMANDS[@]}" ]]; then
echo "Error: Invalid index. Please provide an index between 0 and $((${#TS_SESSION_COMMANDS[@]} - 1))."
exit 1
fi
session_cmd="${TS_SESSION_COMMANDS[$session_idx]}"
shift
;;
--vsplit)
split_type="vsplit"
;;
--hsplit)
split_type="hsplit"
;;
-v | --version)
echo "tmux-sessionizer version $VERSION"
exit 0
;;
--find-dirs)
find_dirs
exit 0
;;
*)
user_selected="$1"
;;
esac
shift
done
log "tmux-sessionizer($VERSION): idx=$session_idx cmd=$session_cmd user_selected=$user_selected split_type=$split_type log=$TS_LOG log_file=$TS_LOG_FILE"
# Validate split options are only used with session commands
if [[ -n "$split_type" && -z "$session_idx" ]]; then
echo "Error: --vsplit and --hsplit can only be used with -s/--session option"
exit 1
fi
sanity_check() {
if ! command -v tmux &>/dev/null; then
echo "tmux is not installed. Please install it first."
exit 1
fi
if ! command -v fzf &>/dev/null; then
echo "fzf is not installed. Please install it first."
exit 1
fi
}
switch_to() {
if [[ -z $TMUX ]]; then
log "attaching to session $1"
tmux attach-session -t "$1"
else
log "switching to session $1"
tmux switch-client -t "$1"
fi
}
has_session() {
tmux list-sessions | grep -q "^$1:"
}
hydrate() {
if [[ ! -z $session_cmd ]]; then
log "skipping hydrate for $1 -- using \"$session_cmd\" instead"
return
elif [ -f "$2/.tmux-sessionizer" ]; then
log "sourcing(local) $2/.tmux-sessionizer"
tmux send-keys -t "$1" "source $2/.tmux-sessionizer" c-M
elif [ -f "$HOME/.tmux-sessionizer" ]; then
log "sourcing(global) $HOME/.tmux-sessionizer"
tmux send-keys -t "$1" "source $HOME/.tmux-sessionizer" c-M
fi
}
is_tmux_running() {
tmux_running=$(pgrep tmux)
if [[ -z $TMUX ]] && [[ -z $tmux_running ]]; then
return 1
fi
return 0
}
init_pane_cache() {
mkdir -p "$PANE_CACHE_DIR"
touch "$PANE_CACHE_FILE"
}
get_pane_id() {
local session_idx="$1"
local split_type="$2"
init_pane_cache
grep "^${session_idx}:${split_type}:" "$PANE_CACHE_FILE" | cut -d: -f3
}
set_pane_id() {
local session_idx="$1"
local split_type="$2"
local pane_id="$3"
init_pane_cache
# Remove existing entry if it exists
grep -v "^${session_idx}:${split_type}:" "$PANE_CACHE_FILE" > "${PANE_CACHE_FILE}.tmp" 2>/dev/null || true
mv "${PANE_CACHE_FILE}.tmp" "$PANE_CACHE_FILE"
# Add new entry
echo "${session_idx}:${split_type}:${pane_id}" >> "$PANE_CACHE_FILE"
}
cleanup_dead_panes() {
init_pane_cache
local temp_file="${PANE_CACHE_FILE}.tmp"
while IFS=: read -r idx split pane_id; do
if tmux list-panes -a -F "#{pane_id}" 2>/dev/null | grep -q "^${pane_id}$"; then
echo "${idx}:${split}:${pane_id}" >> "$temp_file"
fi
done < "$PANE_CACHE_FILE"
mv "$temp_file" "$PANE_CACHE_FILE" 2>/dev/null || touch "$PANE_CACHE_FILE"
}
sanity_check
handle_session_cmd() {
log "executing session command $session_cmd with index $session_idx split_type=$split_type"
if ! is_tmux_running; then
echo "Error: tmux is not running. Please start tmux first before using session commands."
exit 1
fi
current_session=$(tmux display-message -p '#S')
if [[ -n "$split_type" ]]; then
handle_split_session_cmd "$current_session"
else
handle_window_session_cmd "$current_session"
fi
exit 0
}
handle_window_session_cmd() {
local current_session="$1"
start_index=$((69 + $session_idx))
target="$current_session:$start_index"
log "target: $target command $session_cmd has-session=$(tmux has-session -t="$target" 2> /dev/null)"
if tmux has-session -t="$target" 2> /dev/null; then
switch_to "$target"
else
log "executing session command: tmux neww -dt $target $session_cmd"
tmux neww -dt $target "$session_cmd"
hydrate "$target" "$selected"
tmux select-window -t $target
fi
}
handle_split_session_cmd() {
local current_session="$1"
cleanup_dead_panes
# Check if pane already exists
local existing_pane_id=$(get_pane_id "$session_idx" "$split_type")
if [[ -n "$existing_pane_id" ]] && tmux list-panes -a -F "#{pane_id}" 2>/dev/null | grep -q "^${existing_pane_id}$"; then
log "switching to existing pane $existing_pane_id"
tmux select-pane -t "$existing_pane_id"
if [[ -z $TMUX ]]; then
tmux attach-session -t "$current_session"
else
tmux switch-client -t "$current_session"
fi
else
# Create new split
local split_flag=""
if [[ "$split_type" == "vsplit" ]]; then
split_flag="-h" # horizontal layout (vertical split)
else
split_flag="-v" # vertical layout (horizontal split)
fi
log "creating new split: tmux split-window $split_flag -c $(pwd) $session_cmd"
local new_pane_id=$(tmux split-window $split_flag -c "$(pwd)" -P -F "#{pane_id}" "$session_cmd")
if [[ -n "$new_pane_id" ]]; then
set_pane_id "$session_idx" "$split_type" "$new_pane_id"
log "created pane $new_pane_id for session_idx=$session_idx split_type=$split_type"
fi
fi
}
if [[ ! -z $session_cmd ]]; then
handle_session_cmd
elif [[ ! -z $user_selected ]]; then
selected="$user_selected"
else
# selected=$(find_dirs | fzf)
selected=$(find_dirs | fzf \
--bind 'ctrl-d:execute-silent(
[[ {} == \[TMUX\]* ]] && tmux kill-session -t "$(echo {} | cut -d" " -f2-)"
)+reload(tmux-sessionizer --find-dirs)' \
--header 'ENTER: select, CTRL-D: kill session')
fi
if [[ -z $selected ]]; then
exit 0
fi
if [[ "$selected" =~ ^\[TMUX\]\ (.+)$ ]]; then
selected="${BASH_REMATCH[1]}"
fi
selected_name=$(basename "$selected" | tr . _)
if ! has_session "$selected_name"; then
if [[ -z $TMUX ]]; then
# Outside tmux - create attached session to get proper terminal size
tmux new-session -s "$selected_name" -c "$selected" \; \
new-window -c "$selected" \; \
new-window -c "$selected" \; \
send-keys -t "$selected_name:0" "nvim" C-m \; \
send-keys -t "$selected_name:1" "source ~/.zshrc && dev" C-m \; \
select-window -t "$selected_name:0"
hydrate "$selected_name" "$selected"
exit 0
else
# Inside tmux - create detached then switch
tmux new-session -ds "$selected_name" -c "$selected"
tmux new-window -t "$selected_name" -c "$selected"
tmux new-window -t "$selected_name" -c "$selected"
tmux send-keys -t "$selected_name:0" "nvim" C-m
tmux send-keys -t "$selected_name:1" "source ~/.zshrc && dev" C-m
tmux select-window -t "$selected_name:0"
hydrate "$selected_name" "$selected"
fi
fi
switch_to "$selected_name"
#!/usr/bin/env bash
CONFIG_FILE_NAME="tmux-sessionizer.conf"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/tmux-sessionizer"
CONFIG_FILE="$CONFIG_DIR/$CONFIG_FILE_NAME"
PANE_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/tmux-sessionizer"
PANE_CACHE_FILE="$PANE_CACHE_DIR/panes.cache"
# config file example
# ------------------------
# # file: ~/.config/tmux-sessionizer/tmux-sessionizer.conf
# # If set this override the default TS_SEARCH_PATHS (~/personal ~/projects)
# TS_SEARCH_PATHS=(~/)
# # If set this add additional search paths to the default TS_SEARCH_PATHS
# # The number prefix is the depth for the Path [OPTIONAL]
# TS_EXTRA_SEARCH_PATHS=(~/ghq:3 ~/Git:3 ~/.config:2)
# # if set this override the TS_MAX_DEPTH (1)
# TS_MAX_DEPTH=2
# This is not meant to override .tmux-sessionizer. At first i thought this
# would be a good command, but i don't think its ackshually what i want.
#
# Instead, its a list of commands to run on windows who's index is way outside
# of the first 10 windows. This allows you to create as many windows in your
# session as you would like without having your workflow interrupted by these
# programatic windows
#
# how to use:
# tmux-sessionizer -s 0 will execute the first command in window -t 69.
# use single quotes to wrap the command.
# TS_SESSION_COMMANDS=(<cmd1> <cmd2>)
#
# TS_LOG=true # will write logs to ~/.local/share/tmux-sessionizer/tmux-sessionizer.logs
# TS_LOG_FILE=<file> # will write logs to <file> Defaults to ~/.local/share/tmux-sessionizer/tmux-sessionizer.logs
# ------------------------
if [[ -f "$CONFIG_FILE" ]]; then
source "$CONFIG_FILE"
fi
if [[ -f "$CONFIG_FILE_NAME" ]]; then
source "$CONFIG_FILE_NAME"
fi
if [[ $TS_LOG != "true" ]]; then
if [[ -z $TS_LOG_FILE ]]; then
TS_LOG_FILE="$HOME/.local/share/tmux-sessionizer/tmux-sessionizer.logs"
fi
mkdir -p "$(dirname "$TS_LOG_FILE")"
fi
log() {
if [[ -z $TS_LOG ]]; then
return
elif [[ $TS_LOG == "echo" ]]; then
echo "$*"
elif [[ $TS_LOG == "file" ]]; then
echo "$*" >> "$TS_LOG_FILE"
fi
}
# if TS_SEARCH_PATHS is not set use default
[[ -n "$TS_SEARCH_PATHS" ]] || TS_SEARCH_PATHS=(~/personal ~/projects)
# Add any extra search paths to the TS_SEARCH_PATHS array
# e.g : EXTRA_SEARCH_PATHS=("$HOME/extra1:4" "$HOME/extra2")
# note : Path can be suffixed with :number to limit or extend the depth of the search for the Path
if [[ ${#TS_EXTRA_SEARCH_PATHS[@]} -gt 0 ]]; then
TS_SEARCH_PATHS+=("${TS_EXTRA_SEARCH_PATHS[@]}")
fi
# utility function to find directories
find_dirs() {
# list TMUX sessions
if [[ -n "${TMUX}" ]]; then
current_session=$(tmux display-message -p '#S')
tmux list-sessions -F "[TMUX] #{session_name}" 2>/dev/null | grep -vFx "[TMUX] $current_session" | sort -Vr
else
tmux list-sessions -F "[TMUX] #{session_name}" 2>/dev/null | sort -Vr
fi
# note: TS_SEARCH_PATHS is an array of paths to search for directories
# if the path ends with :number, it will search for directories with a max depth of number ;)
# if there is no number, it will search for directories with a max depth defined by TS_MAX_DEPTH or 1 if not set
for entry in "${TS_SEARCH_PATHS[@]}"; do
# Check if entry as :number as suffix then adapt the maxdepth parameter
if [[ "$entry" =~ ^([^:]+):([0-9]+)$ ]]; then
path="${BASH_REMATCH[1]}"
depth="${BASH_REMATCH[2]}"
else
path="$entry"
fi
[[ -d "$path" ]] && find "$path" -mindepth 1 -maxdepth "${depth:-${TS_MAX_DEPTH:-1}}" -path '*/.git' -prune -o -type d -print
done | sort -Vr
}
get_jira_title() {
local ticket="$1"
local cache_file="$HOME/.cache/tmux-sessionizer-jira/$ticket"
if [[ -f "$cache_file" && $(find "$cache_file" -mtime -7 2>/dev/null) ]]; then
cat "$cache_file"
else
mkdir -p "$(dirname "$cache_file")"
local title=$(acli jira workitem view "$ticket" --fields summary 2>/dev/null | tail -1 | sed 's/^[[:space:]]*Summary: //')
echo "$title" > "$cache_file"
echo "$title"
fi
}
add_jira_info() {
while IFS= read -r line; do
if [[ "$line" =~ ^\[TMUX\]\ (.+)$ ]]; then
session_name="${BASH_REMATCH[1]}"
if [[ "$session_name" =~ (CONN-[0-9]+) ]]; then
ticket="${BASH_REMATCH[1]}"
title=$(get_jira_title "$ticket")
if [[ -n "$title" ]]; then
echo "$line [$ticket: $title]"
else
echo "$line [$ticket]"
fi
else
echo "$line"
fi
else
dir_name=$(basename "$line")
if [[ "$dir_name" =~ ^(CONN-[0-9]+) ]]; then
ticket="${BASH_REMATCH[1]}"
title=$(get_jira_title "$ticket")
if [[ -n "$title" ]]; then
echo "$line [$ticket: $title]"
else
echo "$line [$ticket]"
fi
else
echo "$line"
fi
fi
done
}
session_idx=""
session_cmd=""
user_selected=""
split_type=""
VERSION="0.1.0"
while [[ "$#" -gt 0 ]]; do
case "$1" in
-h | --help)
echo "Usage: tmux-sessionizer [OPTIONS] [SEARCH_PATH]"
echo "Options:"
echo " -h, --help Display this help message"
echo " -s, --session <name> session command index."
echo " --vsplit Create vertical split (horizontal layout) for session command"
echo " --hsplit Create horizontal split (vertical layout) for session command"
exit 0
;;
-s | --session)
session_idx="$2"
if [[ -z $session_idx ]]; then
echo "Session index cannot be empty"
exit 1
fi
if [[ -z $TS_SESSION_COMMANDS ]]; then
echo "TS_SESSION_COMMANDS is not set. Must have a command set to run when switching to a session"
exit 1
fi
if [[ -z "$session_idx" || "$session_idx" -lt 0 || "$session_idx" -ge "${#TS_SESSION_COMMANDS[@]}" ]]; then
echo "Error: Invalid index. Please provide an index between 0 and $((${#TS_SESSION_COMMANDS[@]} - 1))."
exit 1
fi
session_cmd="${TS_SESSION_COMMANDS[$session_idx]}"
shift
;;
--vsplit)
split_type="vsplit"
;;
--hsplit)
split_type="hsplit"
;;
-v | --version)
echo "tmux-sessionizer version $VERSION"
exit 0
;;
--find-dirs)
find_dirs
exit 0
;;
--find-dirs-jira)
find_dirs | add_jira_info
exit 0
;;
*)
user_selected="$1"
;;
esac
shift
done
log "tmux-sessionizer($VERSION): idx=$session_idx cmd=$session_cmd user_selected=$user_selected split_type=$split_type log=$TS_LOG log_file=$TS_LOG_FILE"
# Validate split options are only used with session commands
if [[ -n "$split_type" && -z "$session_idx" ]]; then
echo "Error: --vsplit and --hsplit can only be used with -s/--session option"
exit 1
fi
sanity_check() {
if ! command -v tmux &>/dev/null; then
echo "tmux is not installed. Please install it first."
exit 1
fi
if ! command -v fzf &>/dev/null; then
echo "fzf is not installed. Please install it first."
exit 1
fi
}
switch_to() {
if [[ -z $TMUX ]]; then
log "attaching to session $1"
tmux attach-session -t "$1"
else
log "switching to session $1"
tmux switch-client -t "$1"
fi
}
has_session() {
tmux list-sessions | grep -q "^$1:"
}
hydrate() {
if [[ ! -z $session_cmd ]]; then
log "skipping hydrate for $1 -- using \"$session_cmd\" instead"
return
elif [ -f "$2/.tmux-sessionizer" ]; then
log "sourcing(local) $2/.tmux-sessionizer"
tmux send-keys -t "$1" "source $2/.tmux-sessionizer" c-M
elif [ -f "$HOME/.tmux-sessionizer" ]; then
log "sourcing(global) $HOME/.tmux-sessionizer"
tmux send-keys -t "$1" "source $HOME/.tmux-sessionizer" c-M
fi
}
is_tmux_running() {
tmux_running=$(pgrep tmux)
if [[ -z $TMUX ]] && [[ -z $tmux_running ]]; then
return 1
fi
return 0
}
init_pane_cache() {
mkdir -p "$PANE_CACHE_DIR"
touch "$PANE_CACHE_FILE"
}
get_pane_id() {
local session_idx="$1"
local split_type="$2"
init_pane_cache
grep "^${session_idx}:${split_type}:" "$PANE_CACHE_FILE" | cut -d: -f3
}
set_pane_id() {
local session_idx="$1"
local split_type="$2"
local pane_id="$3"
init_pane_cache
# Remove existing entry if it exists
grep -v "^${session_idx}:${split_type}:" "$PANE_CACHE_FILE" > "${PANE_CACHE_FILE}.tmp" 2>/dev/null || true
mv "${PANE_CACHE_FILE}.tmp" "$PANE_CACHE_FILE"
# Add new entry
echo "${session_idx}:${split_type}:${pane_id}" >> "$PANE_CACHE_FILE"
}
cleanup_dead_panes() {
init_pane_cache
local temp_file="${PANE_CACHE_FILE}.tmp"
while IFS=: read -r idx split pane_id; do
if tmux list-panes -a -F "#{pane_id}" 2>/dev/null | grep -q "^${pane_id}$"; then
echo "${idx}:${split}:${pane_id}" >> "$temp_file"
fi
done < "$PANE_CACHE_FILE"
mv "$temp_file" "$PANE_CACHE_FILE" 2>/dev/null || touch "$PANE_CACHE_FILE"
}
sanity_check
handle_session_cmd() {
log "executing session command $session_cmd with index $session_idx split_type=$split_type"
if ! is_tmux_running; then
echo "Error: tmux is not running. Please start tmux first before using session commands."
exit 1
fi
current_session=$(tmux display-message -p '#S')
if [[ -n "$split_type" ]]; then
handle_split_session_cmd "$current_session"
else
handle_window_session_cmd "$current_session"
fi
exit 0
}
handle_window_session_cmd() {
local current_session="$1"
start_index=$((69 + $session_idx))
target="$current_session:$start_index"
log "target: $target command $session_cmd has-session=$(tmux has-session -t="$target" 2> /dev/null)"
if tmux has-session -t="$target" 2> /dev/null; then
switch_to "$target"
else
log "executing session command: tmux neww -dt $target $session_cmd"
tmux neww -dt $target "$session_cmd"
hydrate "$target" "$selected"
tmux select-window -t $target
fi
}
handle_split_session_cmd() {
local current_session="$1"
cleanup_dead_panes
# Check if pane already exists
local existing_pane_id=$(get_pane_id "$session_idx" "$split_type")
if [[ -n "$existing_pane_id" ]] && tmux list-panes -a -F "#{pane_id}" 2>/dev/null | grep -q "^${existing_pane_id}$"; then
log "switching to existing pane $existing_pane_id"
tmux select-pane -t "$existing_pane_id"
if [[ -z $TMUX ]]; then
tmux attach-session -t "$current_session"
else
tmux switch-client -t "$current_session"
fi
else
# Create new split
local split_flag=""
if [[ "$split_type" == "vsplit" ]]; then
split_flag="-h" # horizontal layout (vertical split)
else
split_flag="-v" # vertical layout (horizontal split)
fi
log "creating new split: tmux split-window $split_flag -c $(pwd) $session_cmd"
local new_pane_id=$(tmux split-window $split_flag -c "$(pwd)" -P -F "#{pane_id}" "$session_cmd")
if [[ -n "$new_pane_id" ]]; then
set_pane_id "$session_idx" "$split_type" "$new_pane_id"
log "created pane $new_pane_id for session_idx=$session_idx split_type=$split_type"
fi
fi
}
if [[ ! -z $session_cmd ]]; then
handle_session_cmd
elif [[ ! -z $user_selected ]]; then
selected="$user_selected"
else
selected=$(find_dirs | fzf \
--bind 'start:reload(tmux-sessionizer2 --find-dirs-jira)' \
--bind 'ctrl-d:execute-silent(
session=$(echo {} | sed "s/^\\\[TMUX\\\] \\([^ ]*\\).*/\\1/")
[[ {} == \\[TMUX\\]* ]] && tmux kill-session -t "$session"
)+reload(tmux-sessionizer2 --find-dirs-jira)' \
--header 'ENTER: select, CTRL-D: kill session')
fi
if [[ -z $selected ]]; then
exit 0
fi
if [[ "$selected" =~ ^\[TMUX\]\ (.+)\ \[CONN-[0-9]+: ]]; then
selected="${BASH_REMATCH[1]}"
elif [[ "$selected" =~ ^\[TMUX\]\ (.+)$ ]]; then
selected="${BASH_REMATCH[1]}"
elif [[ "$selected" =~ ^(.+)\ \[CONN-[0-9]+: ]]; then
selected="${BASH_REMATCH[1]}"
fi
selected_name=$(basename "$selected" | tr . _)
if ! is_tmux_running; then
tmux new-session -ds "$selected_name" -c "$selected"
hydrate "$selected_name" "$selected"
fi
if ! has_session "$selected_name"; then
tmux new-session -ds "$selected_name" -c "$selected"
hydrate "$selected_name" "$selected"
fi
switch_to "$selected_name"
vim.keymap.set("n", "<C-f>", "<cmd>silent !tmux neww tmux-sessionizer<CR>")
vim.keymap.set("n", "<M-j>", "<cmd>silent !tmux neww tmux-sessionizer -s 0<CR>")
vim.keymap.set("n", "<M-k>", "<cmd>silent !tmux neww tmux-sessionizer -s 1<CR>")
vim.keymap.set("n", "<M-l>", "<cmd>silent !tmux neww tmux-sessionizer -s 2<CR>")
vim.keymap.set("n", "<M-;>", "<cmd>silent !tmux neww tmux-sessionizer -s 3<CR>")
-- vim: ts=2 sts=2 sw=2 et
#!/usr/bin/env bash
CONFIG_FILE_NAME="tmux-sessionizer.conf"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/tmux-sessionizer"
CONFIG_FILE="$CONFIG_DIR/$CONFIG_FILE_NAME"
PANE_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/tmux-sessionizer"
PANE_CACHE_FILE="$PANE_CACHE_DIR/panes.cache"
# config file example
# ------------------------
# # file: ~/.config/tmux-sessionizer/tmux-sessionizer.conf
# # If set this override the default TS_SEARCH_PATHS (~/personal ~/projects)
# TS_SEARCH_PATHS=(~/)
# # If set this add additional search paths to the default TS_SEARCH_PATHS
# # The number prefix is the depth for the Path [OPTIONAL]
# TS_EXTRA_SEARCH_PATHS=(~/ghq:3 ~/Git:3 ~/.config:2)
# # if set this override the TS_MAX_DEPTH (1)
# TS_MAX_DEPTH=2
# This is not meant to override .tmux-sessionizer. At first i thought this
# would be a good command, but i don't think its ackshually what i want.
#
# Instead, its a list of commands to run on windows who's index is way outside
# of the first 10 windows. This allows you to create as many windows in your
# session as you would like without having your workflow interrupted by these
# programatic windows
#
# how to use:
# tmux-sessionizer -s 0 will execute the first command in window -t 69.
# use single quotes to wrap the command.
# TS_SESSION_COMMANDS=(<cmd1> <cmd2>)
#
# TS_LOG=true # will write logs to ~/.local/share/tmux-sessionizer/tmux-sessionizer.logs
# TS_LOG_FILE=<file> # will write logs to <file> Defaults to ~/.local/share/tmux-sessionizer/tmux-sessionizer.logs
# ------------------------
if [[ -f "$CONFIG_FILE" ]]; then
source "$CONFIG_FILE"
fi
if [[ -f "$CONFIG_FILE_NAME" ]]; then
source "$CONFIG_FILE_NAME"
fi
if [[ $TS_LOG != "true" ]]; then
if [[ -z $TS_LOG_FILE ]]; then
TS_LOG_FILE="$HOME/.local/share/tmux-sessionizer/tmux-sessionizer.logs"
fi
mkdir -p "$(dirname "$TS_LOG_FILE")"
fi
log() {
if [[ -z $TS_LOG ]]; then
return
elif [[ $TS_LOG == "echo" ]]; then
echo "$*"
elif [[ $TS_LOG == "file" ]]; then
echo "$*" >> "$TS_LOG_FILE"
fi
}
# if TS_SEARCH_PATHS is not set use default
[[ -n "$TS_SEARCH_PATHS" ]] || TS_SEARCH_PATHS=(~/personal ~/projects)
# Add any extra search paths to the TS_SEARCH_PATHS array
# e.g : EXTRA_SEARCH_PATHS=("$HOME/extra1:4" "$HOME/extra2")
# note : Path can be suffixed with :number to limit or extend the depth of the search for the Path
if [[ ${#TS_EXTRA_SEARCH_PATHS[@]} -gt 0 ]]; then
TS_SEARCH_PATHS+=("${TS_EXTRA_SEARCH_PATHS[@]}")
fi
# utility function to find directories
find_dirs() {
# list TMUX sessions
if [[ -n "${TMUX}" ]]; then
current_session=$(tmux display-message -p '#S')
tmux list-sessions -F "[TMUX] #{session_name}" 2>/dev/null | grep -vFx "[TMUX] $current_session" | sort -Vr
else
tmux list-sessions -F "[TMUX] #{session_name}" 2>/dev/null | sort -Vr
fi
# note: TS_SEARCH_PATHS is an array of paths to search for directories
# if the path ends with :number, it will search for directories with a max depth of number ;)
# if there is no number, it will search for directories with a max depth defined by TS_MAX_DEPTH or 1 if not set
for entry in "${TS_SEARCH_PATHS[@]}"; do
# Check if entry as :number as suffix then adapt the maxdepth parameter
if [[ "$entry" =~ ^([^:]+):([0-9]+)$ ]]; then
path="${BASH_REMATCH[1]}"
depth="${BASH_REMATCH[2]}"
else
path="$entry"
fi
[[ -d "$path" ]] && find "$path" -mindepth 1 -maxdepth "${depth:-${TS_MAX_DEPTH:-1}}" -path '*/.git' -prune -o -type d -print
done | sort -Vr
}
session_idx=""
session_cmd=""
user_selected=""
split_type=""
VERSION="0.1.0"
while [[ "$#" -gt 0 ]]; do
case "$1" in
-h | --help)
echo "Usage: tmux-sessionizer [OPTIONS] [SEARCH_PATH]"
echo ""
echo "A tmux session manager that creates and switches between project sessions."
echo ""
echo "Options:"
echo " -h, --help Display this help message"
echo " -v, --version Show version information"
echo " -s, --session <index> Execute session command by index (requires TS_SESSION_COMMANDS)"
echo " --vsplit Create vertical split for session command (use with -s)"
echo " --hsplit Create horizontal split for session command (use with -s)"
echo " --find-dirs List all available directories and tmux sessions"
echo ""
echo "Interactive Mode:"
echo " When run without arguments, opens fzf to select from:"
echo " - Existing tmux sessions (prefixed with [TMUX])"
echo " - Project directories from configured search paths"
echo ""
echo " Keybindings in fzf:"
echo " ENTER - Select and switch to session/directory"
echo " CTRL-D - Kill selected tmux session"
echo ""
echo "Configuration:"
echo " Config file: ~/.config/tmux-sessionizer/tmux-sessionizer.conf"
echo " See script comments for configuration options."
echo ""
echo "Examples:"
echo " tmux-sessionizer # Interactive mode"
echo " tmux-sessionizer ~/my-project # Direct path selection"
echo " tmux-sessionizer -s 0 # Execute first session command"
echo " tmux-sessionizer -s 1 --vsplit # Execute second command in vertical split"
exit 0
;;
-s | --session)
session_idx="$2"
if [[ -z $session_idx ]]; then
echo "Session index cannot be empty"
exit 1
fi
if [[ -z $TS_SESSION_COMMANDS ]]; then
echo "TS_SESSION_COMMANDS is not set. Must have a command set to run when switching to a session"
exit 1
fi
if [[ -z "$session_idx" || "$session_idx" -lt 0 || "$session_idx" -ge "${#TS_SESSION_COMMANDS[@]}" ]]; then
echo "Error: Invalid index. Please provide an index between 0 and $((${#TS_SESSION_COMMANDS[@]} - 1))."
exit 1
fi
session_cmd="${TS_SESSION_COMMANDS[$session_idx]}"
shift
;;
--vsplit)
split_type="vsplit"
;;
--hsplit)
split_type="hsplit"
;;
-v | --version)
echo "tmux-sessionizer version $VERSION"
exit 0
;;
--find-dirs)
find_dirs
exit 0
;;
*)
user_selected="$1"
;;
esac
shift
done
log "tmux-sessionizer($VERSION): idx=$session_idx cmd=$session_cmd user_selected=$user_selected split_type=$split_type log=$TS_LOG log_file=$TS_LOG_FILE"
# Validate split options are only used with session commands
if [[ -n "$split_type" && -z "$session_idx" ]]; then
echo "Error: --vsplit and --hsplit can only be used with -s/--session option"
exit 1
fi
sanity_check() {
if ! command -v tmux &>/dev/null; then
echo "tmux is not installed. Please install it first."
exit 1
fi
if ! command -v fzf &>/dev/null; then
echo "fzf is not installed. Please install it first."
exit 1
fi
}
switch_to() {
if [[ -z $TMUX ]]; then
log "attaching to session $1"
tmux attach-session -t "$1"
else
log "switching to session $1"
tmux switch-client -t "$1"
fi
}
has_session() {
tmux list-sessions | grep -q "^$1:"
}
hydrate() {
if [[ ! -z $session_cmd ]]; then
log "skipping hydrate for $1 -- using \"$session_cmd\" instead"
return
elif [ -f "$2/.tmux-sessionizer" ]; then
log "sourcing(local) $2/.tmux-sessionizer"
tmux send-keys -t "$1" "source $2/.tmux-sessionizer" c-M
elif [ -f "$HOME/.tmux-sessionizer" ]; then
log "sourcing(global) $HOME/.tmux-sessionizer"
tmux send-keys -t "$1" "source $HOME/.tmux-sessionizer" c-M
fi
}
is_tmux_running() {
tmux_running=$(pgrep tmux)
if [[ -z $TMUX ]] && [[ -z $tmux_running ]]; then
return 1
fi
return 0
}
init_pane_cache() {
mkdir -p "$PANE_CACHE_DIR"
touch "$PANE_CACHE_FILE"
}
get_pane_id() {
local session_idx="$1"
local split_type="$2"
init_pane_cache
grep "^${session_idx}:${split_type}:" "$PANE_CACHE_FILE" | cut -d: -f3
}
set_pane_id() {
local session_idx="$1"
local split_type="$2"
local pane_id="$3"
init_pane_cache
# Remove existing entry if it exists
grep -v "^${session_idx}:${split_type}:" "$PANE_CACHE_FILE" > "${PANE_CACHE_FILE}.tmp" 2>/dev/null || true
mv "${PANE_CACHE_FILE}.tmp" "$PANE_CACHE_FILE"
# Add new entry
echo "${session_idx}:${split_type}:${pane_id}" >> "$PANE_CACHE_FILE"
}
cleanup_dead_panes() {
init_pane_cache
local temp_file="${PANE_CACHE_FILE}.tmp"
while IFS=: read -r idx split pane_id; do
if tmux list-panes -a -F "#{pane_id}" 2>/dev/null | grep -q "^${pane_id}$"; then
echo "${idx}:${split}:${pane_id}" >> "$temp_file"
fi
done < "$PANE_CACHE_FILE"
mv "$temp_file" "$PANE_CACHE_FILE" 2>/dev/null || touch "$PANE_CACHE_FILE"
}
sanity_check
handle_session_cmd() {
log "executing session command $session_cmd with index $session_idx split_type=$split_type"
if ! is_tmux_running; then
echo "Error: tmux is not running. Please start tmux first before using session commands."
exit 1
fi
current_session=$(tmux display-message -p '#S')
if [[ -n "$split_type" ]]; then
handle_split_session_cmd "$current_session"
else
handle_window_session_cmd "$current_session"
fi
exit 0
}
handle_window_session_cmd() {
local current_session="$1"
start_index=$((69 + $session_idx))
target="$current_session:$start_index"
log "target: $target command $session_cmd has-session=$(tmux has-session -t="$target" 2> /dev/null)"
if tmux has-session -t="$target" 2> /dev/null; then
switch_to "$target"
else
log "executing session command: tmux neww -dt $target $session_cmd"
tmux neww -dt $target "$session_cmd"
hydrate "$target" "$selected"
tmux select-window -t $target
fi
}
handle_split_session_cmd() {
local current_session="$1"
cleanup_dead_panes
# Check if pane already exists
local existing_pane_id=$(get_pane_id "$session_idx" "$split_type")
if [[ -n "$existing_pane_id" ]] && tmux list-panes -a -F "#{pane_id}" 2>/dev/null | grep -q "^${existing_pane_id}$"; then
log "switching to existing pane $existing_pane_id"
tmux select-pane -t "$existing_pane_id"
if [[ -z $TMUX ]]; then
tmux attach-session -t "$current_session"
else
tmux switch-client -t "$current_session"
fi
else
# Create new split
local split_flag=""
if [[ "$split_type" == "vsplit" ]]; then
split_flag="-h" # horizontal layout (vertical split)
else
split_flag="-v" # vertical layout (horizontal split)
fi
log "creating new split: tmux split-window $split_flag -c $(pwd) $session_cmd"
local new_pane_id=$(tmux split-window $split_flag -c "$(pwd)" -P -F "#{pane_id}" "$session_cmd")
if [[ -n "$new_pane_id" ]]; then
set_pane_id "$session_idx" "$split_type" "$new_pane_id"
log "created pane $new_pane_id for session_idx=$session_idx split_type=$split_type"
fi
fi
}
if [[ ! -z $session_cmd ]]; then
handle_session_cmd
elif [[ ! -z $user_selected ]]; then
selected="$user_selected"
else
# selected=$(find_dirs | fzf)
selected=$(find_dirs | fzf \
--bind 'ctrl-d:execute-silent(
[[ {} == \[TMUX\]* ]] && tmux kill-session -t "$(echo {} | cut -d" " -f2-)"
)+reload(tmux-sessionizer --find-dirs)' \
--header 'ENTER: select, CTRL-D: kill session')
fi
if [[ -z $selected ]]; then
exit 0
fi
if [[ "$selected" =~ ^\[TMUX\]\ (.+)$ ]]; then
selected="${BASH_REMATCH[1]}"
fi
selected_name=$(basename "$selected" | tr . _)
if ! is_tmux_running; then
tmux new-session -ds "$selected_name" -c "$selected"
hydrate "$selected_name" "$selected"
fi
if ! has_session "$selected_name"; then
tmux new-session -ds "$selected_name" -c "$selected"
# Create two additional windows (total of 3)
tmux new-window -t "$selected_name" -c "$selected" # Window 1
tmux new-window -t "$selected_name" -c "$selected" # Window 2
# Execute commands in specific windows with proper shell loading
tmux send-keys -t "$selected_name:0" "nvim" C-m # Window 0: nvim
tmux send-keys -t "$selected_name:1" "source ~/.zshrc && dev" C-m # Window 1: dev with zshrc loaded
# Window 2 remains empty with shell
# Select the first window
tmux select-window -t "$selected_name:0"
hydrate "$selected_name" "$selected"
fi
switch_to "$selected_name"
#!/usr/bin/env bash
CONFIG_FILE_NAME="tmux-sessionizer.conf"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/tmux-sessionizer"
CONFIG_FILE="$CONFIG_DIR/$CONFIG_FILE_NAME"
PANE_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/tmux-sessionizer"
PANE_CACHE_FILE="$PANE_CACHE_DIR/panes.cache"
# config file example
# ------------------------
# # file: ~/.config/tmux-sessionizer/tmux-sessionizer.conf
# # If set this override the default TS_SEARCH_PATHS (~/personal ~/projects)
# TS_SEARCH_PATHS=(~/)
# # If set this add additional search paths to the default TS_SEARCH_PATHS
# # The number prefix is the depth for the Path [OPTIONAL]
# TS_EXTRA_SEARCH_PATHS=(~/ghq:3 ~/Git:3 ~/.config:2)
# # if set this override the TS_MAX_DEPTH (1)
# TS_MAX_DEPTH=2
# This is not meant to override .tmux-sessionizer. At first i thought this
# would be a good command, but i don't think its ackshually what i want.
#
# Instead, its a list of commands to run on windows who's index is way outside
# of the first 10 windows. This allows you to create as many windows in your
# session as you would like without having your workflow interrupted by these
# programatic windows
#
# how to use:
# tmux-sessionizer -s 0 will execute the first command in window -t 69.
# use single quotes to wrap the command.
# TS_SESSION_COMMANDS=(<cmd1> <cmd2>)
#
# TS_LOG=true # will write logs to ~/.local/share/tmux-sessionizer/tmux-sessionizer.logs
# TS_LOG_FILE=<file> # will write logs to <file> Defaults to ~/.local/share/tmux-sessionizer/tmux-sessionizer.logs
# ------------------------
if [[ -f "$CONFIG_FILE" ]]; then
source "$CONFIG_FILE"
fi
if [[ -f "$CONFIG_FILE_NAME" ]]; then
source "$CONFIG_FILE_NAME"
fi
if [[ $TS_LOG != "true" ]]; then
if [[ -z $TS_LOG_FILE ]]; then
TS_LOG_FILE="$HOME/.local/share/tmux-sessionizer/tmux-sessionizer.logs"
fi
mkdir -p "$(dirname "$TS_LOG_FILE")"
fi
log() {
if [[ -z $TS_LOG ]]; then
return
elif [[ $TS_LOG == "echo" ]]; then
echo "$*"
elif [[ $TS_LOG == "file" ]]; then
echo "$*" >> "$TS_LOG_FILE"
fi
}
# if TS_SEARCH_PATHS is not set use default
[[ -n "$TS_SEARCH_PATHS" ]] || TS_SEARCH_PATHS=(~/personal ~/projects)
# Add any extra search paths to the TS_SEARCH_PATHS array
# e.g : EXTRA_SEARCH_PATHS=("$HOME/extra1:4" "$HOME/extra2")
# note : Path can be suffixed with :number to limit or extend the depth of the search for the Path
if [[ ${#TS_EXTRA_SEARCH_PATHS[@]} -gt 0 ]]; then
TS_SEARCH_PATHS+=("${TS_EXTRA_SEARCH_PATHS[@]}")
fi
# utility function to find directories
find_dirs() {
# list TMUX sessions
if [[ -n "${TMUX}" ]]; then
current_session=$(tmux display-message -p '#S')
tmux list-sessions -F "[TMUX] #{session_name}" 2>/dev/null | grep -vFx "[TMUX] $current_session" | sort -Vr
else
tmux list-sessions -F "[TMUX] #{session_name}" 2>/dev/null | sort -Vr
fi
# note: TS_SEARCH_PATHS is an array of paths to search for directories
# if the path ends with :number, it will search for directories with a max depth of number ;)
# if there is no number, it will search for directories with a max depth defined by TS_MAX_DEPTH or 1 if not set
for entry in "${TS_SEARCH_PATHS[@]}"; do
# Check if entry as :number as suffix then adapt the maxdepth parameter
if [[ "$entry" =~ ^([^:]+):([0-9]+)$ ]]; then
path="${BASH_REMATCH[1]}"
depth="${BASH_REMATCH[2]}"
else
path="$entry"
fi
[[ -d "$path" ]] && find "$path" -mindepth 1 -maxdepth "${depth:-${TS_MAX_DEPTH:-1}}" -path '*/.git' -prune -o -type d -print
done | sort -Vr
}
get_jira_title() {
local ticket="$1"
local cache_file="$HOME/.cache/tmux-sessionizer-jira/$ticket"
if [[ -f "$cache_file" && $(find "$cache_file" -mtime -7 2>/dev/null) ]]; then
cat "$cache_file"
else
mkdir -p "$(dirname "$cache_file")"
local title=$(acli jira workitem view "$ticket" --fields summary 2>/dev/null | tail -1 | sed 's/^[[:space:]]*Summary: //')
echo "$title" > "$cache_file"
echo "$title"
fi
}
add_jira_info() {
while IFS= read -r line; do
if [[ "$line" =~ ^\[TMUX\]\ (.+)$ ]]; then
session_name="${BASH_REMATCH[1]}"
if [[ "$session_name" =~ (CONN-[0-9]+) ]]; then
ticket="${BASH_REMATCH[1]}"
title=$(get_jira_title "$ticket")
if [[ -n "$title" ]]; then
echo "$line [$ticket: $title]"
else
echo "$line [$ticket]"
fi
else
echo "$line"
fi
else
dir_name=$(basename "$line")
if [[ "$dir_name" =~ ^(CONN-[0-9]+) ]]; then
ticket="${BASH_REMATCH[1]}"
title=$(get_jira_title "$ticket")
if [[ -n "$title" ]]; then
echo "$line [$ticket: $title]"
else
echo "$line [$ticket]"
fi
else
echo "$line"
fi
fi
done
}
session_idx=""
session_cmd=""
user_selected=""
split_type=""
VERSION="0.1.0"
while [[ "$#" -gt 0 ]]; do
case "$1" in
-h | --help)
echo "Usage: tmux-sessionizer [OPTIONS] [SEARCH_PATH]"
echo "Options:"
echo " -h, --help Display this help message"
echo " -s, --session <name> session command index."
echo " --vsplit Create vertical split (horizontal layout) for session command"
echo " --hsplit Create horizontal split (vertical layout) for session command"
exit 0
;;
-s | --session)
session_idx="$2"
if [[ -z $session_idx ]]; then
echo "Session index cannot be empty"
exit 1
fi
if [[ -z $TS_SESSION_COMMANDS ]]; then
echo "TS_SESSION_COMMANDS is not set. Must have a command set to run when switching to a session"
exit 1
fi
if [[ -z "$session_idx" || "$session_idx" -lt 0 || "$session_idx" -ge "${#TS_SESSION_COMMANDS[@]}" ]]; then
echo "Error: Invalid index. Please provide an index between 0 and $((${#TS_SESSION_COMMANDS[@]} - 1))."
exit 1
fi
session_cmd="${TS_SESSION_COMMANDS[$session_idx]}"
shift
;;
--vsplit)
split_type="vsplit"
;;
--hsplit)
split_type="hsplit"
;;
-v | --version)
echo "tmux-sessionizer version $VERSION"
exit 0
;;
--find-dirs)
find_dirs
exit 0
;;
--find-dirs-jira)
find_dirs | add_jira_info
exit 0
;;
*)
user_selected="$1"
;;
esac
shift
done
log "tmux-sessionizer($VERSION): idx=$session_idx cmd=$session_cmd user_selected=$user_selected split_type=$split_type log=$TS_LOG log_file=$TS_LOG_FILE"
# Validate split options are only used with session commands
if [[ -n "$split_type" && -z "$session_idx" ]]; then
echo "Error: --vsplit and --hsplit can only be used with -s/--session option"
exit 1
fi
sanity_check() {
if ! command -v tmux &>/dev/null; then
echo "tmux is not installed. Please install it first."
exit 1
fi
if ! command -v fzf &>/dev/null; then
echo "fzf is not installed. Please install it first."
exit 1
fi
}
switch_to() {
if [[ -z $TMUX ]]; then
log "attaching to session $1"
tmux attach-session -t "$1"
else
log "switching to session $1"
tmux switch-client -t "$1"
fi
}
has_session() {
tmux list-sessions | grep -q "^$1:"
}
hydrate() {
if [[ ! -z $session_cmd ]]; then
log "skipping hydrate for $1 -- using \"$session_cmd\" instead"
return
elif [ -f "$2/.tmux-sessionizer" ]; then
log "sourcing(local) $2/.tmux-sessionizer"
tmux send-keys -t "$1" "source $2/.tmux-sessionizer" c-M
elif [ -f "$HOME/.tmux-sessionizer" ]; then
log "sourcing(global) $HOME/.tmux-sessionizer"
tmux send-keys -t "$1" "source $HOME/.tmux-sessionizer" c-M
fi
}
is_tmux_running() {
tmux_running=$(pgrep tmux)
if [[ -z $TMUX ]] && [[ -z $tmux_running ]]; then
return 1
fi
return 0
}
init_pane_cache() {
mkdir -p "$PANE_CACHE_DIR"
touch "$PANE_CACHE_FILE"
}
get_pane_id() {
local session_idx="$1"
local split_type="$2"
init_pane_cache
grep "^${session_idx}:${split_type}:" "$PANE_CACHE_FILE" | cut -d: -f3
}
set_pane_id() {
local session_idx="$1"
local split_type="$2"
local pane_id="$3"
init_pane_cache
# Remove existing entry if it exists
grep -v "^${session_idx}:${split_type}:" "$PANE_CACHE_FILE" > "${PANE_CACHE_FILE}.tmp" 2>/dev/null || true
mv "${PANE_CACHE_FILE}.tmp" "$PANE_CACHE_FILE"
# Add new entry
echo "${session_idx}:${split_type}:${pane_id}" >> "$PANE_CACHE_FILE"
}
cleanup_dead_panes() {
init_pane_cache
local temp_file="${PANE_CACHE_FILE}.tmp"
while IFS=: read -r idx split pane_id; do
if tmux list-panes -a -F "#{pane_id}" 2>/dev/null | grep -q "^${pane_id}$"; then
echo "${idx}:${split}:${pane_id}" >> "$temp_file"
fi
done < "$PANE_CACHE_FILE"
mv "$temp_file" "$PANE_CACHE_FILE" 2>/dev/null || touch "$PANE_CACHE_FILE"
}
sanity_check
handle_session_cmd() {
log "executing session command $session_cmd with index $session_idx split_type=$split_type"
if ! is_tmux_running; then
echo "Error: tmux is not running. Please start tmux first before using session commands."
exit 1
fi
current_session=$(tmux display-message -p '#S')
if [[ -n "$split_type" ]]; then
handle_split_session_cmd "$current_session"
else
handle_window_session_cmd "$current_session"
fi
exit 0
}
handle_window_session_cmd() {
local current_session="$1"
start_index=$((69 + $session_idx))
target="$current_session:$start_index"
log "target: $target command $session_cmd has-session=$(tmux has-session -t="$target" 2> /dev/null)"
if tmux has-session -t="$target" 2> /dev/null; then
switch_to "$target"
else
log "executing session command: tmux neww -dt $target $session_cmd"
tmux neww -dt $target "$session_cmd"
hydrate "$target" "$selected"
tmux select-window -t $target
fi
}
handle_split_session_cmd() {
local current_session="$1"
cleanup_dead_panes
# Check if pane already exists
local existing_pane_id=$(get_pane_id "$session_idx" "$split_type")
if [[ -n "$existing_pane_id" ]] && tmux list-panes -a -F "#{pane_id}" 2>/dev/null | grep -q "^${existing_pane_id}$"; then
log "switching to existing pane $existing_pane_id"
tmux select-pane -t "$existing_pane_id"
if [[ -z $TMUX ]]; then
tmux attach-session -t "$current_session"
else
tmux switch-client -t "$current_session"
fi
else
# Create new split
local split_flag=""
if [[ "$split_type" == "vsplit" ]]; then
split_flag="-h" # horizontal layout (vertical split)
else
split_flag="-v" # vertical layout (horizontal split)
fi
log "creating new split: tmux split-window $split_flag -c $(pwd) $session_cmd"
local new_pane_id=$(tmux split-window $split_flag -c "$(pwd)" -P -F "#{pane_id}" "$session_cmd")
if [[ -n "$new_pane_id" ]]; then
set_pane_id "$session_idx" "$split_type" "$new_pane_id"
log "created pane $new_pane_id for session_idx=$session_idx split_type=$split_type"
fi
fi
}
if [[ ! -z $session_cmd ]]; then
handle_session_cmd
elif [[ ! -z $user_selected ]]; then
selected="$user_selected"
else
selected=$(find_dirs | fzf \
--bind 'start:reload(tmux-sessionizer2 --find-dirs-jira)' \
--bind 'ctrl-d:execute-silent(
session=$(echo {} | sed "s/^\\\[TMUX\\\] \\([^ ]*\\).*/\\1/")
[[ {} == \\[TMUX\\]* ]] && tmux kill-session -t "$session"
)+reload(tmux-sessionizer2 --find-dirs-jira)' \
--header 'ENTER: select, CTRL-D: kill session')
fi
if [[ -z $selected ]]; then
exit 0
fi
if [[ "$selected" =~ ^\[TMUX\]\ (.+)\ \[CONN-[0-9]+: ]]; then
selected="${BASH_REMATCH[1]}"
elif [[ "$selected" =~ ^\[TMUX\]\ (.+)$ ]]; then
selected="${BASH_REMATCH[1]}"
elif [[ "$selected" =~ ^(.+)\ \[CONN-[0-9]+: ]]; then
selected="${BASH_REMATCH[1]}"
fi
selected_name=$(basename "$selected" | tr . _)
if ! is_tmux_running; then
tmux new-session -ds "$selected_name" -c "$selected"
hydrate "$selected_name" "$selected"
fi
if ! has_session "$selected_name"; then
tmux new-session -ds "$selected_name" -c "$selected"
hydrate "$selected_name" "$selected"
fi
switch_to "$selected_name"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment