diff --git a/_demos/mouse.go b/_demos/mouse.go index e31503b..fd51b27 100644 --- a/_demos/mouse.go +++ b/_demos/mouse.go @@ -123,6 +123,7 @@ func main() { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } + s.SetTitle("Tcell Mouse Demonstration") defStyle = tcell.StyleDefault. Background(tcell.ColorReset). Foreground(tcell.ColorReset) diff --git a/_demos/unicode.go b/_demos/unicode.go index 246459d..a23d61e 100644 --- a/_demos/unicode.go +++ b/_demos/unicode.go @@ -109,6 +109,9 @@ func main() { Background(tcell.ColorWhite)) s.Clear() + // we can even try to use unicode window titles! + s.SetTitle("Unicode Demonstration -- 🤯") + quit := make(chan struct{}) style = bold diff --git a/console_win.go b/console_win.go index 7db6663..573c153 100644 --- a/console_win.go +++ b/console_win.go @@ -42,6 +42,7 @@ type cScreen struct { truecolor bool running bool disableAlt bool // disable the alternate screen + title string w int h int @@ -176,6 +177,9 @@ const ( vtExitUrl = "\x1b]8;;\x1b\\" vtCursorColorRGB = "\x1b]12;#%02x%02x%02x\007" vtCursorColorReset = "\x1b]112\007" + vtSaveTitle = "\x1b[22;2t" + vtRestoreTitle = "\x1b[23;2t" + vtSetTitle = "\x1b]2;%s\x1b\\" ) var vtCursorStyles = map[CursorStyle]string{ @@ -350,6 +354,7 @@ func (s *cScreen) disengage() { s.emitVtString(vtCursorColorReset) s.emitVtString(vtEnableAm) if !s.disableAlt { + s.emitVtString(vtRestoreTitle) s.emitVtString(vtExitCA) } } else if !s.disableAlt { @@ -387,9 +392,13 @@ func (s *cScreen) engage() error { if s.vten { s.setOutMode(modeVtOutput | modeNoAutoNL | modeCookedOut | modeUnderline) if !s.disableAlt { + s.emitVtString(vtSaveTitle) s.emitVtString(vtEnterCA) } s.emitVtString(vtDisableAm) + if s.title != "" { + s.emitVtString(fmt.Sprintf(vtSetTitle, s.title)) + } } else { s.setOutMode(0) } @@ -1275,6 +1284,15 @@ func (s *cScreen) SetStyle(style Style) { s.Unlock() } +func (s *cScreen) SetTitle(title string) { + s.Lock() + s.title = title + if s.vten { + s.emitVtString(fmt.Sprintf(vtSetTitle, title)) + } + s.Unlock() +} + // No fallback rune support, since we have Unicode. Yay! func (s *cScreen) RegisterRuneFallback(_ rune, _ string) { diff --git a/screen.go b/screen.go index b2b94e1..69f7bdf 100644 --- a/screen.go +++ b/screen.go @@ -266,6 +266,12 @@ type Screen interface { // Tty returns the underlying Tty. If the screen is not a terminal, the // returned bool will be false Tty() (Tty, bool) + + // SetTitle sets a window title on the screen. + // Terminals may be configured to ignore this, or unable to. + // Tcell may attempt to save and restore the window title on entry and exit, but + // the results may vary. Use of unicode characters may not be supported. + SetTitle(string) } // NewScreen returns a default Screen suitable for the user's terminal @@ -335,6 +341,7 @@ type screenImpl interface { Resume() error Beep() error SetSize(int, int) + SetTitle(string) Tty() (Tty, bool) // Following methods are not part of the Screen api, but are used for interaction with diff --git a/sim_test.go b/sim_test.go index 0c1949f..8086984 100644 --- a/sim_test.go +++ b/sim_test.go @@ -150,3 +150,13 @@ func TestBeep(t *testing.T) { } } } + +func TestTitle(t *testing.T) { + s := mkTestScreen(t, "") + defer s.Fini() + s.SetTitle("My Title") + s.Show() + if s.GetTitle() != "My Title" { + t.Errorf("Title mismatched") + } +} diff --git a/simulation.go b/simulation.go index 2017292..e2c2957 100644 --- a/simulation.go +++ b/simulation.go @@ -60,6 +60,9 @@ type SimulationScreen interface { // GetCursor returns the cursor details. GetCursor() (x int, y int, visible bool) + + // GetTitle gets the set title + GetTitle() string } // SimCell represents a simulated screen cell. The purpose of this @@ -98,6 +101,7 @@ type simscreen struct { fillchar rune fillstyle Style fallback map[rune]string + title string Screen sync.Mutex @@ -495,3 +499,11 @@ func (s *simscreen) EventQ() chan Event { func (s *simscreen) StopQ() <-chan struct{} { return s.quit } + +func (s *simscreen) SetTitle(title string) { + s.title = title +} + +func (s *simscreen) GetTitle() string { + return s.title +} diff --git a/terminfo/terminfo.go b/terminfo/terminfo.go index 7f2879b..44fefc5 100644 --- a/terminfo/terminfo.go +++ b/terminfo/terminfo.go @@ -233,6 +233,7 @@ type Terminfo struct { EnterUrl string ExitUrl string SetWindowSize string + SetWindowTitle string // no terminfo extension EnableFocusReporting string DisableFocusReporting string DisableAutoMargin string // smam diff --git a/tscreen.go b/tscreen.go index 5bcc713..f648435 100644 --- a/tscreen.go +++ b/tscreen.go @@ -171,6 +171,10 @@ type tScreen struct { mouseFlags MouseFlags pasteEnabled bool focusEnabled bool + setTitle string + saveTitle string + restoreTitle string + title string sync.Mutex } @@ -414,27 +418,37 @@ func (t *tScreen) prepareExtendedOSC() { if t.ti.EnterUrl != "" { t.enterUrl = t.ti.EnterUrl t.exitUrl = t.ti.ExitUrl - } else if t.ti.Mouse != "" { + } else if t.ti.Mouse != "" || t.ti.XTermLike { t.enterUrl = "\x1b]8;%p2%s;%p1%s\x1b\\" t.exitUrl = "\x1b]8;;\x1b\\" } if t.ti.SetWindowSize != "" { t.setWinSize = t.ti.SetWindowSize - } else if t.ti.Mouse != "" { + } else if t.ti.Mouse != "" || t.ti.XTermLike { t.setWinSize = "\x1b[8;%p1%p2%d;%dt" } if t.ti.EnableFocusReporting != "" { t.enableFocus = t.ti.EnableFocusReporting - } else if t.ti.Mouse != "" { + } else if t.ti.Mouse != "" || t.ti.XTermLike { t.enableFocus = "\x1b[?1004h" } if t.ti.DisableFocusReporting != "" { t.disableFocus = t.ti.DisableFocusReporting - } else if t.ti.Mouse != "" { + } else if t.ti.Mouse != "" || t.ti.XTermLike { t.disableFocus = "\x1b[?1004l" } + + if t.ti.SetWindowTitle != "" { + t.setTitle = t.ti.SetWindowTitle + } else if t.ti.XTermLike { + t.saveTitle = "\x1b[22;2t" + t.restoreTitle = "\x1b[23;2t" + // this also tries to request that UTF-8 is allowed in the title + t.setTitle = "\x1b[>2t\x1b]2;%p1%s\x1b\\" + + } } func (t *tScreen) prepareCursorStyles() { @@ -1938,12 +1952,18 @@ func (t *tScreen) engage() error { // (In theory there could be terminals that don't support X,Y cursor // positions without a setup command, but we don't support them.) t.TPuts(ti.EnterCA) + if t.saveTitle != "" { + t.TPuts(t.saveTitle) + } } t.TPuts(ti.EnterKeypad) t.TPuts(ti.HideCursor) t.TPuts(ti.EnableAcs) t.TPuts(ti.DisableAutoMargin) t.TPuts(ti.Clear) + if t.title != "" && t.setTitle != "" { + t.TPuts(t.ti.TParm(t.setTitle, t.title)) + } t.wg.Add(2) go t.inputLoop(stopQ) @@ -1987,6 +2007,9 @@ func (t *tScreen) disengage() { t.TPuts(ti.ExitKeypad) t.TPuts(ti.EnableAutoMargin) if os.Getenv("TCELL_ALTSCREEN") != "disable" { + if t.restoreTitle != "" { + t.TPuts(t.restoreTitle) + } t.TPuts(ti.Clear) // only needed if ExitCA is empty t.TPuts(ti.ExitCA) } @@ -2021,3 +2044,12 @@ func (t *tScreen) EventQ() chan Event { func (t *tScreen) GetCells() *CellBuffer { return &t.cells } + +func (t *tScreen) SetTitle(title string) { + t.Lock() + t.title = title + if t.setTitle != "" && t.running { + t.TPuts(t.ti.TParm(t.setTitle, title)) + } + t.Unlock() +} diff --git a/webfiles/tcell.js b/webfiles/tcell.js index 3319d09..5e17b52 100644 --- a/webfiles/tcell.js +++ b/webfiles/tcell.js @@ -222,6 +222,10 @@ function beep() { beepAudio.play(); } +function setTitle(title) { + document.title = title; +} + function intToHex(n) { return "#" + n.toString(16).padStart(6, "0"); } diff --git a/wscreen.go b/wscreen.go index 6d1b2e4..ebcb7e9 100644 --- a/wscreen.go +++ b/wscreen.go @@ -523,6 +523,10 @@ func (t *wScreen) StopQ() <-chan struct{} { return t.quit } +func (t *wScreen) SetTitle(title string) { + js.Global().Call("setTitle", title) +} + // WebKeyNames maps string names reported from HTML // (KeyboardEvent.key) to tcell accepted keys. var WebKeyNames = map[string]Key{