termtitle 0.1.0

Intelligently sets terminal window/tab titles based on configurable rules
Documentation

termtitle

Automatically sets terminal window and tab titles based on configurable rules as you cd between directories.

Installation

cargo install termtitle

Or build from source:

cargo build --release
cp target/release/termtitle ~/.local/bin/  # or somewhere in your PATH

Add the shell hook to your shell config:

Zsh (~/.zshrc):

eval "$(termtitle hook zsh)"

Bash (~/.bashrc):

eval "$(termtitle hook bash)"

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

termtitle hook <shell>    # Output shell hook (zsh, bash, or fish)
termtitle apply           # Apply title based on rules for current directory
termtitle reset           # Reset title to default
termtitle inspect         # Show what title would be set and why
termtitle set <target> <title>  # Directly set a terminal title
termtitle config          # Show current configuration
termtitle config --edit   # Open config file in $EDITOR
termtitle config --path   # Print config file path

How It Works

  1. Shell hook calls termtitle apply on every directory change
  2. apply evaluates rules in config order, first match wins
  3. Matching rule provides template variables (name, branch, etc.)
  4. Template is rendered and emitted as OSC escape sequences

Configuration

Configuration is stored at ~/.config/termtitle/config.toml:

shell_timeout_ms = 500      # Timeout for shell_command rules (milliseconds)
fallback_title = "{dir}"    # Title when no rules match
default_targets = ["both"]  # Options: "window", "tab", "both"

# Rules are evaluated in order, first match wins
[[rules]]
filename = "package.json"
path = "name"

[[rules]]
filename = "Cargo.toml"
path = "package.name"

[[rules]]
type = "git"
template = "{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.

[[rules]]
filename = "package.json"
path = "name"                    # Single value → {value} or {name}
template = "{name}"

[[rules]]
filename = "Cargo.toml"
path = "package.name"

Multiple values with paths:

[[rules]]
filename = "package.json"
paths = ["name", "version"]      # Creates {name} and {version}
template = "{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 directories
  • search = "current" — only current directory
  • search = "parent" — skip current, search parents only

Git Repository

[[rules]]
type = "git"
template = "{repo}:{branch}"

Variables: {repo}, {branch}, {commit}, {status} (shows * if dirty), {remote}, {upstream}

Directory Name

[[rules]]
type = "directory_name"
search = "current"
template = "{dir}"

File Exists

[[rules]]
type = "file_exists"
filename = ".termtitle"
use_content = true               # Read first line into {content}
template = "{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:

[[rules]]
type = "file_exists"
filename = "*.xcodeproj"
template = "{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

[[rules]]
type = "env_var"
env_name = "PROJECT_NAME"
template = "{value}"

Shell Command

[[rules]]
type = "shell_command"
command = "basename $(pwd)"
timeout_ms = 200                 # Override global timeout
template = "{value}"

Template Syntax

Templates use {variable} placeholders that are replaced with values from rule providers.

Basic Variables

template = "{name}"              # Simple variable
template = "{repo}:{branch}"     # Multiple variables

Fallback Values

Use {var:fallback} to provide a default when the variable is missing:

template = "{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:

template = "{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.

[[rules]]
type = "compound"
template = "{pkg.name}{ [{git.branch}]}"
require_all = false              # At least one component must match (default)

  [rules.components.pkg]
  filename = "package.json"
  path = "name"

  [rules.components.git]
  type = "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:

[rules.components.pkg]
filename = "package.json"
paths = ["name", "version"]      # Creates {pkg.name} and {pkg.version}

Component Search Modes

Each component can have its own search mode, useful for monorepos:

[[rules]]
type = "compound"
template = "{workspace.name} → {project.name}"

  [rules.components.workspace]
  filename = "package.json"
  path = "name"
  search = "parent"              # Find workspace root

  [rules.components.project]
  filename = "package.json"
  path = "name"
  search = "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
[[rules]]
filename = "package.json"
path = "name"
template = "{name}"
targets = ["tab"]

# Full context in window
[[rules]]
type = "git"
template = "{repo}:{branch}"
targets = ["window"]

Examples

Web Development

# Package name in tab, git context in window
[[rules]]
filename = "package.json"
path = "name"
targets = ["tab"]

[[rules]]
type = "git"
template = "{repo}:{branch}"
targets = ["window"]

Microservices with Environment

[[rules]]
type = "compound"
template = "{pkg.name}{ • {env:dev}}"
require_all = false

  [rules.components.pkg]
  filename = "package.json"
  path = "name"

  [rules.components.env]
  type = "env_var"
  env_name = "NODE_ENV"

Monorepo (Workspace + Package)

[[rules]]
type = "compound"
template = "{workspace.name} → {project.name}"
require_all = false

  [rules.components.workspace]
  filename = "package.json"
  path = "name"
  search = "parent"

  [rules.components.project]
  filename = "package.json"
  path = "name"
  search = "current"

Git with Dirty Indicator

[[rules]]
type = "git"
template = "{repo}:{branch}{status}"  # Shows * when dirty

Version-Tagged Project

[[rules]]
filename = "package.json"
paths = ["name", "version"]
template = "{name}{ v{version}}"

Multi-Language Detection

[[rules]]
filename = "package.json"
path = "name"
template = "{name} [Node]"

[[rules]]
filename = "Cargo.toml"
path = "package.name"
template = "{value} [Rust]"

[[rules]]
filename = "pyproject.toml"
path = "project.name"
template = "{value} [Python]"

[[rules]]
type = "git"
template = "{repo}"

Custom Project Marker

echo "my-project" > ~/Code/my-project/.termtitle
[[rules]]
type = "file_exists"
filename = ".termtitle"
use_content = true
template = "{content}"

DevOps/Infrastructure

[[rules]]
type = "file_exists"
filename = "main.tf"
template = "{dir} [Terraform]"

[[rules]]
filename = "Chart.yaml"
path = "name"
template = "{value} [Helm]"

Fallback Chain Pattern

When you need strict matching with graceful fallback:

# First: try with all fields (strict)
[[rules]]
filename = "package.json"
paths = ["name", "version", "author"]
template = "{name} v{version} by {author}"

# Fallback: just name and version
[[rules]]
filename = "package.json"
paths = ["name", "version"]
template = "{name} v{version}"

Or use conditional segments for the same effect in one rule:

[[rules]]
filename = "package.json"
paths = ["name", "version", "author"]
template = "{name} v{version}{ by {author}}"

License

MIT