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,