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: