termtitle
Automatically sets terminal window and tab titles based on configurable rules as you cd between directories.
Installation
Or build from source:
Add the shell hook to your shell config:
Zsh (~/.zshrc):
Bash (~/.bashrc):
Fish (~/.config/fish/config.fish):
termtitle hook fish | source
Usage
termtitle uses a rule-based system to determine titles. Rules are evaluated in order, and the first match wins.
Note: termtitle uses OSC escape sequences for setting terminal titles. These work in most modern terminal emulators including iTerm2, Terminal.app, Windows Terminal, and others.
Commands
How It Works
- Shell hook calls
termtitle applyon every directory change applyevaluates rules in config order, first match wins- Matching rule provides template variables (name, branch, etc.)
- Template is rendered and emitted as OSC escape sequences
Configuration
Configuration is stored at ~/.config/termtitle/config.toml:
= 500 # Timeout for shell_command rules (milliseconds)
= "{dir}" # Title when no rules match
= ["both"] # Options: "window", "tab", "both"
# Rules are evaluated in order, first match wins
[[]]
= "package.json"
= "name"
[[]]
= "Cargo.toml"
= "package.name"
[[]]
= "git"
= "{repo}:{branch}"
Rule Types
Each rule type detects specific conditions and provides template variables.
Structured Files (JSON/TOML)
Read values from JSON or TOML files. Type is inferred from extension when omitted.
[[]]
= "package.json"
= "name" # Single value → {value} or {name}
= "{name}"
[[]]
= "Cargo.toml"
= "package.name"
Multiple values with paths:
[[]]
= "package.json"
= ["name", "version"] # Creates {name} and {version}
= "{name}{ v{version}}" # Use conditional for optional version
With paths, all specified paths must exist for the rule to match (unless used
in conditional segments).
Search modes:
search = "up"(default) — search current and parent directoriessearch = "current"— only current directorysearch = "parent"— skip current, search parents only
Git Repository
[[]]
= "git"
= "{repo}:{branch}"
Variables: {repo}, {branch}, {commit}, {status} (shows * if dirty),
{remote}, {upstream}
Directory Name
[[]]
= "directory_name"
= "current"
= "{dir}"
File Exists
[[]]
= "file_exists"
= ".termtitle"
= true # Read first line into {content}
= "{content}"
Variables: {dir}, {filename}, {content} (if use_content = true)
Glob pattern matching:
Use glob patterns to match files dynamically. When a pattern matches, the {match} variable contains the matched portion without extension:
[[]]
= "file_exists"
= "*.xcodeproj"
= "{match}" # Shows "MyApp" for "MyApp.xcodeproj"
Variables:
{match}— matched portion without extension (e.g.,"MyApp"from"MyApp.xcodeproj"){filename}— original pattern (e.g.,"*.xcodeproj"){dir}— directory name
Exact filenames (no wildcards) still work for backward compatibility.
Environment Variable
[[]]
= "env_var"
= "PROJECT_NAME"
= "{value}"
Shell Command
[[]]
= "shell_command"
= "basename $(pwd)"
= 200 # Override global timeout
= "{value}"
Template Syntax
Templates use {variable} placeholders that are replaced with values from rule
providers.
Basic Variables
= "{name}" # Simple variable
= "{repo}:{branch}" # Multiple variables
Fallback Values
Use {var:fallback} to provide a default when the variable is missing:
= "{env:production}" # Shows "production" if env is unset
Conditional Segments
Use { segment } (space after opening brace) to hide content when variables are
missing. If any variable in the segment lacks a fallback, the entire segment
disappears:
= "{name}{ v{version}}{ by {author}}"
# ^required ^optional ^optional
Output examples:
- All present:
"myapp v1.0.0 by Tom" - No author:
"myapp v1.0.0" - No version or author:
"myapp"
Format Modifiers
Apply transformations using {var|modifier} or {var|modifier:arg}:
| Modifier | Arguments | Example | Result |
|---|---|---|---|
truncate |
max length | {name|truncate:20} |
"very-long-name-he..." |
upper |
none | {branch|upper} |
"MAIN" |
lower |
none | {name|lower} |
"myapp" |
title |
none | {name|title} |
"My App" |
icon |
value=symbol map | {status|icon:ok=✓,fail=✗} |
"✓" |
prefix |
string | {branch|prefix:[} |
"[main" |
suffix |
string | {branch|suffix:]} |
"main]" |
basename |
none | {dir|basename} |
"project" |
dirname |
none | {path|dirname} |
"/home/user" |
ext |
none | {file|ext} |
"rs" |
stem |
none | {file|stem} |
"main" |
parent |
depth (default 1) | {dir|parent:2} |
grandparent name |
Modifiers can be chained: {dir|basename|upper} → "PROJECT"
Compound Rules
Compound rules merge multiple providers into a single title, combining data from different sources.
[[]]
= "compound"
= "{pkg.name}{ [{git.branch}]}"
= false # At least one component must match (default)
[]
= "package.json"
= "name"
[]
= "git"
How It Works
- Each component is a nested rule configuration
- Component names become variable prefixes:
{pkg.name},{git.branch} - With
require_all = false: at least one component must match - With
require_all = true: all components must match or rule is skipped
Multiple Values from One File
Use paths (plural) to extract multiple values:
[]
= "package.json"
= ["name", "version"] # Creates {pkg.name} and {pkg.version}
Component Search Modes
Each component can have its own search mode, useful for monorepos:
[[]]
= "compound"
= "{workspace.name} → {project.name}"
[]
= "package.json"
= "name"
= "parent" # Find workspace root
[]
= "package.json"
= "name"
= "current" # Current package only
Title Targets
termtitle can set different terminal title elements:
| Target | Description | OSC Code |
|---|---|---|
window |
Window title | OSC 2 |
tab |
Tab/icon name | OSC 1 |
both |
Both window and tab | OSC 0 |
Rules continue evaluating until all targets are satisfied:
# Short name in tab
[[]]
= "package.json"
= "name"
= "{name}"
= ["tab"]
# Full context in window
[[]]
= "git"
= "{repo}:{branch}"
= ["window"]
Examples
Web Development
# Package name in tab, git context in window
[[]]
= "package.json"
= "name"
= ["tab"]
[[]]
= "git"
= "{repo}:{branch}"
= ["window"]
Microservices with Environment
[[]]
= "compound"
= "{pkg.name}{ • {env:dev}}"
= false
[]
= "package.json"
= "name"
[]
= "env_var"
= "NODE_ENV"
Monorepo (Workspace + Package)
[[]]
= "compound"
= "{workspace.name} → {project.name}"
= false
[]
= "package.json"
= "name"
= "parent"
[]
= "package.json"
= "name"
= "current"
Git with Dirty Indicator
[[]]
= "git"
= "{repo}:{branch}{status}" # Shows * when dirty
Version-Tagged Project
[[]]
= "package.json"
= ["name", "version"]
= "{name}{ v{version}}"
Multi-Language Detection
[[]]
= "package.json"
= "name"
= "{name} [Node]"
[[]]
= "Cargo.toml"
= "package.name"
= "{value} [Rust]"
[[]]
= "pyproject.toml"
= "project.name"
= "{value} [Python]"
[[]]
= "git"
= "{repo}"
Custom Project Marker
[[]]
= "file_exists"
= ".termtitle"
= true
= "{content}"
DevOps/Infrastructure
[[]]
= "file_exists"
= "main.tf"
= "{dir} [Terraform]"
[[]]
= "Chart.yaml"
= "name"
= "{value} [Helm]"
Fallback Chain Pattern
When you need strict matching with graceful fallback:
# First: try with all fields (strict)
[[]]
= "package.json"
= ["name", "version", "author"]
= "{name} v{version} by {author}"
# Fallback: just name and version
[[]]
= "package.json"
= ["name", "version"]
= "{name} v{version}"
Or use conditional segments for the same effect in one rule:
[[]]
= "package.json"
= ["name", "version", "author"]
= "{name} v{version}{ by {author}}"
License
MIT