# termtitle
Automatically sets terminal window and tab titles based on configurable rules as
you cd between directories.
## Installation
```bash
cargo install termtitle
```
Or build from source:
```bash
cargo build --release
cp target/release/termtitle ~/.local/bin/ # or somewhere in your PATH
```
Add the shell hook to your shell config:
**Zsh** (`~/.zshrc`):
```zsh
eval "$(termtitle hook zsh)"
```
**Bash** (`~/.bashrc`):
```bash
eval "$(termtitle hook bash)"
```
**Fish** (`~/.config/fish/config.fish`):
```fish
## 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
```bash
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`:
```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.
```toml
[[rules]]
filename = "package.json"
path = "name" # Single value → {value} or {name}
template = "{name}"
[[rules]]
filename = "Cargo.toml"
path = "package.name"
```
**Multiple values with `paths`:**
```toml
[[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
```toml
[[rules]]
type = "git"
template = "{repo}:{branch}"
```
Variables: `{repo}`, `{branch}`, `{commit}`, `{status}` (shows `*` if dirty),
`{remote}`, `{upstream}`
### Directory Name
```toml
[[rules]]
type = "directory_name"
search = "current"
template = "{dir}"
```
### File Exists
```toml
[[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:
```toml
[[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
```toml
[[rules]]
type = "env_var"
env_name = "PROJECT_NAME"
template = "{value}"
```
### Shell Command
```toml
[[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
```toml
template = "{name}" # Simple variable
template = "{repo}:{branch}" # Multiple variables
```
### Fallback Values
Use `{var:fallback}` to provide a default when the variable is missing:
```toml
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:
```toml
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}`:
| `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.
```toml
[[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:
```toml
[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:
```toml
[[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:
| `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:
```toml
# 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
```toml
# 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
```toml
[[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)
```toml
[[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
```toml
[[rules]]
type = "git"
template = "{repo}:{branch}{status}" # Shows * when dirty
```
### Version-Tagged Project
```toml
[[rules]]
filename = "package.json"
paths = ["name", "version"]
template = "{name}{ v{version}}"
```
### Multi-Language Detection
```toml
[[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
```bash
echo "my-project" > ~/Code/my-project/.termtitle
```
```toml
[[rules]]
type = "file_exists"
filename = ".termtitle"
use_content = true
template = "{content}"
```
### DevOps/Infrastructure
```toml
[[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:
```toml
# 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:
```toml
[[rules]]
filename = "package.json"
paths = ["name", "version", "author"]
template = "{name} v{version}{ by {author}}"
```
## License
MIT