diff --git a/_demos/mouse.go b/_demos/mouse.go index 394808f4..1047678e 100644 --- a/_demos/mouse.go +++ b/_demos/mouse.go @@ -129,12 +129,14 @@ func main() { s.SetStyle(defStyle) s.EnableMouse() s.EnablePaste() + s.EnableFocus() s.Clear() posfmt := "Mouse: %d, %d " btnfmt := "Buttons: %s" keyfmt := "Keys: %s" pastefmt := "Paste: [%d] %s" + focusfmt := "Focus: %s" white := tcell.StyleDefault. Foreground(tcell.ColorMidnightBlue).Background(tcell.ColorLightCoral) @@ -148,9 +150,10 @@ func main() { pstr := "" ecnt := 0 pasting := false + focus := true // assume we are focused when we start for { - drawBox(s, 1, 1, 42, 7, white, ' ') + drawBox(s, 1, 1, 42, 8, white, ' ') emitStr(s, 2, 2, white, "Press ESC twice to exit, C to clear.") emitStr(s, 2, 3, white, fmt.Sprintf(posfmt, mx, my)) emitStr(s, 2, 4, white, fmt.Sprintf(btnfmt, bstr)) @@ -162,6 +165,12 @@ func main() { } emitStr(s, 2, 6, white, fmt.Sprintf(pastefmt, len(pstr), ps)) + fstr := "false" + if focus { + fstr = "true" + } + emitStr(s, 2, 7, white, fmt.Sprintf(focusfmt, fstr)) + s.Show() bstr = "" ev := s.PollEvent() @@ -315,6 +324,8 @@ func main() { lchar = ch s.SetContent(w-1, h-1, 'M', nil, st) mx, my = x, y + case *tcell.EventFocus: + focus = ev.Focused default: s.SetContent(w-1, h-1, 'X', nil, st) } diff --git a/console_win.go b/console_win.go index 5f0063e2..01fa1f7b 100644 --- a/console_win.go +++ b/console_win.go @@ -282,6 +282,10 @@ func (s *cScreen) EnablePaste() {} func (s *cScreen) DisablePaste() {} +func (s *cScreen) EnableFocus() {} + +func (s *cScreen) DisableFocus() {} + func (s *cScreen) Fini() { s.disengage() } diff --git a/focus.go b/focus.go new file mode 100644 index 00000000..e9b93ef6 --- /dev/null +++ b/focus.go @@ -0,0 +1,28 @@ +// Copyright 2023 The TCell Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use file except in compliance with the License. +// You may obtain a copy of the license at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tcell + +// EventFocus is a focus event. It is sent when the terminal window (or tab) +// gets or loses focus. +type EventFocus struct { + *EventTime + + // True if the window received focus, false if it lost focus + Focused bool +} + +func NewEventFocus(focused bool) *EventFocus { + return &EventFocus{Focused: focused} +} diff --git a/screen.go b/screen.go index 5598a8c7..bd35a89d 100644 --- a/screen.go +++ b/screen.go @@ -139,6 +139,12 @@ type Screen interface { // DisablePaste disables bracketed paste mode. DisablePaste() + // EnableFocus enables reporting of focus events, if your terminal supports it. + EnableFocus() + + // DisableFocus disables reporting of focus events. + DisableFocus() + // HasMouse returns true if the terminal (apparently) supports a // mouse. Note that the return value of true doesn't guarantee that // a mouse/pointing device is present; a false return definitely diff --git a/simulation.go b/simulation.go index 9ad6131e..b18b6648 100644 --- a/simulation.go +++ b/simulation.go @@ -325,6 +325,12 @@ func (s *simscreen) DisablePaste() { s.paste = false } +func (s *simscreen) EnableFocus() { +} + +func (s *simscreen) DisableFocus() { +} + func (s *simscreen) Size() (int, int) { s.Lock() w, h := s.back.Size() diff --git a/terminfo/terminfo.go b/terminfo/terminfo.go index cf157864..c248dd49 100644 --- a/terminfo/terminfo.go +++ b/terminfo/terminfo.go @@ -230,6 +230,8 @@ type Terminfo struct { EnterUrl string ExitUrl string SetWindowSize string + EnableFocusReporting string + DisableFocusReporting string } const ( diff --git a/tscreen.go b/tscreen.go index e36e6e41..27635d38 100644 --- a/tscreen.go +++ b/tscreen.go @@ -153,6 +153,8 @@ type tScreen struct { enterUrl string exitUrl string setWinSize string + enableFocus string + disableFocus string cursorStyles map[CursorStyle]string cursorStyle CursorStyle saved *term.State @@ -161,6 +163,7 @@ type tScreen struct { wg sync.WaitGroup mouseFlags MouseFlags pasteEnabled bool + focusEnabled bool sync.Mutex } @@ -366,6 +369,17 @@ func (t *tScreen) prepareExtendedOSC() { } else if t.ti.Mouse != "" { t.setWinSize = "\x1b[8;%p1%p2%d;%dt" } + + if t.ti.EnableFocusReporting != "" { + t.enableFocus = t.ti.EnableFocusReporting + } else if t.ti.Mouse != "" { + t.enableFocus = "\x1b[?1004h" + } + if t.ti.DisableFocusReporting != "" { + t.disableFocus = t.ti.DisableFocusReporting + } else if t.ti.Mouse != "" { + t.disableFocus = "\x1b[?1004l" + } } func (t *tScreen) prepareCursorStyles() { @@ -1043,6 +1057,32 @@ func (t *tScreen) enablePasting(on bool) { } } +func (t *tScreen) EnableFocus() { + t.Lock() + t.focusEnabled = true + t.enableFocusReporting() + t.Unlock() +} + +func (t *tScreen) DisableFocus() { + t.Lock() + t.focusEnabled = false + t.disableFocusReporting() + t.Unlock() +} + +func (t *tScreen) enableFocusReporting() { + if t.enableFocus != "" { + t.TPuts(t.enableFocus) + } +} + +func (t *tScreen) disableFocusReporting() { + if t.disableFocus != "" { + t.TPuts(t.disableFocus) + } +} + func (t *tScreen) Size() (int, int) { t.Lock() w, h := t.w, t.h @@ -1380,6 +1420,35 @@ func (t *tScreen) parseSgrMouse(buf *bytes.Buffer, evs *[]Event) (bool, bool) { return true, false } +func (t *tScreen) parseFocus(buf *bytes.Buffer, evs *[]Event) (bool, bool) { + state := 0 + b := buf.Bytes() + for i := range b { + switch state { + case 0: + if b[i] != '\x1b' { + return false, false + } + state = 1 + case 1: + if b[i] != '[' { + return false, false + } + state = 2 + case 2: + if b[i] != 'I' && b[i] != 'O' { + return false, false + } + *evs = append(*evs, NewEventFocus(b[i] == 'I')) + _, _ = buf.ReadByte() + _, _ = buf.ReadByte() + _, _ = buf.ReadByte() + return true, true + } + } + return true, false +} + // parseXtermMouse is like parseSgrMouse, but it parses a legacy // X11 mouse record. func (t *tScreen) parseXtermMouse(buf *bytes.Buffer, evs *[]Event) (bool, bool) { @@ -1556,6 +1625,12 @@ func (t *tScreen) collectEventsFromInput(buf *bytes.Buffer, expire bool) []Event partials++ } + if part, comp := t.parseFocus(buf, &res); comp { + continue + } else if part { + partials++ + } + // Only parse mouse records if this term claims to have // mouse support @@ -1804,6 +1879,9 @@ func (t *tScreen) engage() error { t.stopQ = stopQ t.enableMouse(t.mouseFlags) t.enablePasting(t.pasteEnabled) + if t.focusEnabled { + t.enableFocusReporting() + } ti := t.ti t.TPuts(ti.EnterCA) @@ -1853,6 +1931,7 @@ func (t *tScreen) disengage() { t.TPuts(ti.ExitKeypad) t.enableMouse(0) t.enablePasting(false) + t.disableFocusReporting() _ = t.tty.Stop() }