feat: add configurable terminal window title

Add a `gui.terminalTitle` config option (default: `lazygit::{{repoName}}`)
that sets the terminal window title via ANSI escape sequences through
gocui.Screen.SetTitle(). The title updates on startup and when switching
repos. Control characters are sanitized to prevent escape sequence
injection. Set to empty string to disable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rabin Yasharzadehe 2026-04-07 16:04:25 +03:00
parent c07f4d381b
commit 8730d58ba6
6 changed files with 124 additions and 3 deletions

View file

@ -358,6 +358,11 @@ gui:
# is already active, go to next tab instead
switchTabsWithPanelJumpKeys: false
# Format string for the terminal window title. Supports placeholders:
# - {{repoName}}: Name of the current repository
# Set to empty string to disable terminal title updates.
terminalTitle: lazygit::{{repoName}}
# Config relating to git
git:
# Array of diff renderers. Each entry has the following format:

View file

@ -222,6 +222,10 @@ type GuiConfig struct {
SwitchToFilesAfterStashApply bool `yaml:"switchToFilesAfterStashApply"`
// If true, when using the panel jump keys (default 1 through 5) and target panel is already active, go to next tab instead
SwitchTabsWithPanelJumpKeys bool `yaml:"switchTabsWithPanelJumpKeys"`
// Format string for the terminal window title. Supports placeholders:
// - {{repoName}}: Name of the current repository
// Set to empty string to disable terminal title updates.
TerminalTitle string `yaml:"terminalTitle"`
}
func (c *GuiConfig) UseFuzzySearch() bool {
@ -934,6 +938,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
SwitchToFilesAfterStashPop: true,
SwitchToFilesAfterStashApply: true,
SwitchTabsWithPanelJumpKeys: false,
TerminalTitle: "lazygit::{{repoName}}",
},
Git: GitConfig{
Commit: CommitConfig{

View file

@ -533,6 +533,8 @@ func (gui *Gui) onUserConfigLoaded() error {
presentation.SetCustomBranches(userConfig.Gui.BranchColors, false)
}
gui.updateTerminalTitle()
return nil
}
@ -1141,13 +1143,35 @@ func (gui *Gui) loadNewRepo() error {
}
refresh(options)
if err := gui.os.UpdateWindowTitle(); err != nil {
return err
}
gui.updateTerminalTitle()
return nil
}
func (gui *Gui) updateTerminalTitle() {
titleFormat := gui.c.UserConfig().Gui.TerminalTitle
if titleFormat == "" {
return
}
// gui.git may not be set yet during initial startup
if gui.git == nil || gui.git.RepoPaths == nil {
return
}
repoName := gui.git.RepoPaths.RepoName()
title := utils.ResolvePlaceholderString(titleFormat, map[string]string{
"repoName": repoName,
})
// Sanitize title by removing control characters that could break terminal behavior
title = utils.SanitizeTerminalTitle(title)
if gocui.Screen != nil {
gocui.Screen.SetTitle(title)
}
}
func (gui *Gui) showIntroPopupMessage() {
gui.waitForIntro.Add(1)

View file

@ -31,3 +31,15 @@ func ResolvePlaceholderString(str string, arguments map[string]string) string {
}
return strings.NewReplacer(oldnews...).Replace(str)
}
// SanitizeTerminalTitle removes control characters from a string intended
// for use as a terminal title. Control characters (ASCII 0-31 and 127) could
// break terminal behavior or be used for escape sequence injection.
func SanitizeTerminalTitle(title string) string {
return strings.Map(func(r rune) rune {
if r < 32 || r == 127 {
return -1 // Remove control characters
}
return r
}, title)
}

View file

@ -66,3 +66,73 @@ func TestResolvePlaceholderString(t *testing.T) {
assert.EqualValues(t, s.expected, ResolvePlaceholderString(s.templateString, s.arguments))
}
}
func TestSanitizeTerminalTitle(t *testing.T) {
scenarios := []struct {
name string
input string
expected string
}{
{
name: "normal string",
input: "lazygit::myproject",
expected: "lazygit::myproject",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "string with spaces",
input: "lazygit :: my project",
expected: "lazygit :: my project",
},
{
name: "string with unicode",
input: "lazygit::项目",
expected: "lazygit::项目",
},
{
name: "removes null byte",
input: "lazy\x00git",
expected: "lazygit",
},
{
name: "removes escape sequence",
input: "lazy\x1b[31mgit",
expected: "lazy[31mgit",
},
{
name: "removes newline and tab",
input: "lazy\ngit\ttitle",
expected: "lazygittitle",
},
{
name: "removes carriage return",
input: "lazy\rgit",
expected: "lazygit",
},
{
name: "removes DEL character",
input: "lazy\x7fgit",
expected: "lazygit",
},
{
name: "removes bell character",
input: "lazy\x07git",
expected: "lazygit",
},
{
name: "preserves printable special chars",
input: "lazy&git|test<>",
expected: "lazy&git|test<>",
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
assert.EqualValues(t, s.expected, SanitizeTerminalTitle(s.input))
})
}
}

View file

@ -925,6 +925,11 @@
"type": "boolean",
"description": "If true, when using the panel jump keys (default 1 through 5) and target panel is already active, go to next tab instead",
"default": false
},
"terminalTitle": {
"type": "string",
"description": "Format string for the terminal window title. Supports placeholders:\n- {{repoName}}: Name of the current repository\nSet to empty string to disable terminal title updates.",
"default": "lazygit::{{repoName}}"
}
},
"additionalProperties": false,