nobe4 / Posts / Custom Kitty Tab Titles _

  |   Tech

I changed my Kitty tab bar to show a better context:

For simplicity, it updates only when something changes (cd, command start, prompt).

OSC Escape Sequences 🔗

ANSI escape sequences are sequences starting with ESC (\e), ending with BEL (\a) that control terminal behavior. They include:

OSC Syntax 🔗

ESC ] Ps ; Pt BEL
PsEffect
0Set icon name + window title
1Set icon name only
2Set window title only
21Set color
52Set clipboard content
99Send a notification

E.g.

printf '\e]0;hello\a'                       # set window title to "hello"
printf '\e]21;cursor=blue\a'                # set the cursor blue
printf "\e]52;c;$(echo "hello" | base64)\a" # set the clipboard content
printf '\e]99;;Hello world\a'               # send notification

See ANSI list for more.

The Title Function 🔗

Here’s the full code I came up with:

# functions/set_tab_title
local cwd="$PWD"
local path=""

# try git info
local remote branch
remote=$(git remote get-url origin 2>/dev/null)
if [[ -n "$remote" ]]; then
  branch=$(git branch --show-current 2>/dev/null)
  remote="${remote%.git}"
  if [[ "$remote" == *@*:* ]]; then
    # git@github.com:owner/repo → owner/repo
    path="${remote##*:}:${branch}"
  else
    # https://github.com/owner/repo → owner/repo
    local owner_repo="${remote%/*}"
    owner_repo="${owner_repo##*/}/${remote##*/}"
    path="${owner_repo}:${branch}"
  fi
else
  # shorten: ~/dev/nobe4/dotfiles → ~/d/n/dotfiles
  local short="${cwd/#$HOME/~}"
  local parts=("${(@s:/:)short}")
  local last="${parts[-1]}"
  local result=""
  for ((i=1; i<${#parts[@]}; i++)); do
    result+="${parts[$i][1]}/"
  done
  path="${result}${last}"
fi

local proc="${1:-${ZSH_NAME:-zsh}}"

print -Pn "\e]0;${path} ${proc}\a"

The function uses several zsh-specific substitution patterns:

#/## trims the prefix, %/%% trims the suffix. Single #/% removes the shortest match, double ##/%% removes the longest match.

Hooking It Up 🔗

Zsh has hook function arrays that fire at specific moments:

HookWhen it fires
precmd_functionsBefore each prompt display
preexec_functionsBefore each command executes
chpwd_functionsAfter directory change

E.g.

function exec-smth {
	printf "\e]99;;executing $1\a"
}
preexec_functions+=(exec-smth)

function changed-dir {
	printf 'changed dir'
}
chpwd_functions+=(changed-dir)

Zsh can also autoload functions from files in $fpath. The file content is the function body (no wrapper needed), so set_tab_title lives as a file:

# in .zshrc
autoload -U functions/*(:t)

precmd_functions+=(set_tab_title)
chpwd_functions+=(set_tab_title)
preexec_functions+=(set_tab_title)

Kitty Configuration 🔗

Kitty comes with shell integration and many settings, the interesting ones here are:

# don't let kitty's shell integration override our title
shell_integration no-title

# use the OSC-set title in the tab bar template
tab_title_template "{title}"

shell_integration with no-title disables kitty’s title management, which would otherwise overwrite the OSC escape we set.

tab_title_template has {title} in the template which is the window’s OSC-set title. Compare with {tab.active_exe} which is just the process name.

Final flow 🔗

  1. User types cd ~/project or runs nvim

  2. Zsh fires chpwd/preexec hook

  3. set_tab_title computes “owner/repo:branch process”

    print -Pn "\e]0;nobe4/dotfiles:main nvim\a"
  4. kitty receives OSC 0, stores as window title and sets the tab title.

References: