From 38b01889912fea47743b56fa9faa8146dff75a23 Mon Sep 17 00:00:00 2001 From: Rabin Yasharzadehe Date: Tue, 7 Apr 2026 16:04:25 +0300 Subject: [PATCH] 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) --- docs-master/Config.md | 5 ++ .../oscommands/os_default_platform.go | 4 -- pkg/commands/oscommands/os_windows.go | 12 ---- pkg/config/user_config.go | 5 ++ pkg/gui/gui.go | 30 +++++++- pkg/utils/template.go | 12 ++++ pkg/utils/template_test.go | 70 +++++++++++++++++++ schema-master/config.json | 5 ++ 8 files changed, 124 insertions(+), 19 deletions(-) diff --git a/docs-master/Config.md b/docs-master/Config.md index 9f79218217f..ab04e653e04 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -338,6 +338,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 pagers. Each entry has the following format: diff --git a/pkg/commands/oscommands/os_default_platform.go b/pkg/commands/oscommands/os_default_platform.go index 06684434e7e..f4ab4171ac3 100644 --- a/pkg/commands/oscommands/os_default_platform.go +++ b/pkg/commands/oscommands/os_default_platform.go @@ -36,10 +36,6 @@ func getUserShell() string { return "bash" } -func (c *OSCommand) UpdateWindowTitle() error { - return nil -} - func TerminateProcessGracefully(cmd *exec.Cmd) error { if cmd.Process == nil { return nil diff --git a/pkg/commands/oscommands/os_windows.go b/pkg/commands/oscommands/os_windows.go index 605ed768275..60e534a9653 100644 --- a/pkg/commands/oscommands/os_windows.go +++ b/pkg/commands/oscommands/os_windows.go @@ -1,10 +1,7 @@ package oscommands import ( - "fmt" - "os" "os/exec" - "path/filepath" ) func GetPlatform() *Platform { @@ -15,15 +12,6 @@ func GetPlatform() *Platform { } } -func (c *OSCommand) UpdateWindowTitle() error { - path, getWdErr := os.Getwd() - if getWdErr != nil { - return getWdErr - } - argString := fmt.Sprint("title ", filepath.Base(path), " - Lazygit") - return c.Cmd.NewShell(argString, c.UserConfig().OS.ShellFunctionsFile).Run() -} - func TerminateProcessGracefully(cmd *exec.Cmd) error { // Signals other than SIGKILL are not supported on Windows return nil diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index cac87ec9157..092fefb333a 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -206,6 +206,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 { @@ -889,6 +893,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig { SwitchToFilesAfterStashPop: true, SwitchToFilesAfterStashApply: true, SwitchTabsWithPanelJumpKeys: false, + TerminalTitle: "lazygit::{{repoName}}", }, Git: GitConfig{ Commit: CommitConfig{ diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index e2881cca148..975d866b4fd 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -508,6 +508,8 @@ func (gui *Gui) onUserConfigLoaded() error { presentation.SetCustomBranches(userConfig.Gui.BranchColors, false) } + gui.updateTerminalTitle() + return nil } @@ -1068,13 +1070,35 @@ func (gui *Gui) loadNewRepo() error { gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) - 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) diff --git a/pkg/utils/template.go b/pkg/utils/template.go index f5fea99e443..4a61742ba7a 100644 --- a/pkg/utils/template.go +++ b/pkg/utils/template.go @@ -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) +} diff --git a/pkg/utils/template_test.go b/pkg/utils/template_test.go index 236c2327804..f96132aba83 100644 --- a/pkg/utils/template_test.go +++ b/pkg/utils/template_test.go @@ -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)) + }) + } +} diff --git a/schema-master/config.json b/schema-master/config.json index 2e968ba8f95..c0d3e4ec029 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -842,6 +842,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,