diff --git a/commands.go b/commands.go index 0cb6da710c..199ce95dcf 100644 --- a/commands.go +++ b/commands.go @@ -2,6 +2,8 @@ package tea import ( "time" + + "github.com/charmbracelet/x/ansi" ) // Batch performs a bunch of commands concurrently with no ordering guarantees @@ -215,24 +217,36 @@ func WindowSize() Cmd { } } -// setEnhancedKeyboardMsg is a message to enable/disable enhanced keyboard -// features. -type setEnhancedKeyboardMsg bool +type enableKeyboardEnhancementsMsg []KeyboardEnhancement -// EnableEnhancedKeyboard is a command to enable enhanced keyboard features. -// This unambiguously reports more key combinations than traditional terminal -// keyboard sequences. This might also enable reporting of release key events -// depending on the terminal emulator supporting it. -// -// This is equivalent to calling EnablieKittyKeyboard(3) and -// EnableModifyOtherKeys(1). -func EnableEnhancedKeyboard() Msg { - return setEnhancedKeyboardMsg(true) +// EnableKeyboardEnhancements is a command that enables keyboard enhancements +// in the terminal. +func EnableKeyboardEnhancements(enhancements ...KeyboardEnhancement) Cmd { + enhancements = append(enhancements, func(k *keyboardEnhancements) { + k.kittyFlags |= ansi.KittyDisambiguateEscapeCodes + if k.modifyOtherKeys < 1 { + k.modifyOtherKeys = 1 + } + }) + return func() Msg { + return enableKeyboardEnhancementsMsg(enhancements) + } } -// DisableEnhancedKeyboard is a command to disable enhanced keyboard features. -// -// This is equivalent to calling DisableKittyKeyboard() and DisableModifyOtherKeys(). -func DisableEnhancedKeyboard() Msg { - return setEnhancedKeyboardMsg(false) +type disableKeyboardEnhancementsMsg struct{} + +// DisableKeyboardEnhancements is a command that disables keyboard enhancements +// in the terminal. +func DisableKeyboardEnhancements() Msg { + return disableKeyboardEnhancementsMsg{} +} + +// KeyboardEnhancementsMsg is a message that gets sent when the terminal +// supports keyboard enhancements. +type KeyboardEnhancementsMsg keyboardEnhancements + +// SupportsReleaseKeys returns whether the terminal supports key release +// events. +func (k KeyboardEnhancementsMsg) SupportsReleaseKeys() bool { + return k.kittyFlags&ansi.KittyReportEventTypes != 0 } diff --git a/examples/print-key/main.go b/examples/print-key/main.go index 801b5ac1f6..7369e1ba64 100644 --- a/examples/print-key/main.go +++ b/examples/print-key/main.go @@ -14,12 +14,17 @@ func (m model) Init() (tea.Model, tea.Cmd) { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case tea.KeyPressMsg: - switch msg.String() { - case "ctrl+c": - return m, tea.Quit + case tea.KeyboardEnhancementsMsg: + return m, tea.Printf("Keyboard enhancements enabled! ReleaseKeys: %v\n", msg.SupportsReleaseKeys()) + case tea.KeyMsg: + switch msg := msg.(type) { + case tea.KeyPressMsg: + switch msg.String() { + case "ctrl+c": + return m, tea.Quit + } } - return m, tea.Printf("You pressed: %s\n", msg.String()) + return m, tea.Printf("You pressed: %s (%T)\n", msg.String(), msg) } return m, nil } @@ -29,7 +34,7 @@ func (m model) View() string { } func main() { - p := tea.NewProgram(model{}, tea.WithEnhancedKeyboard()) + p := tea.NewProgram(model{}, tea.WithKeyboardEnhancements(tea.WithReleaseKeys)) if _, err := p.Run(); err != nil { log.Printf("Error running program: %v", err) } diff --git a/kitty.go b/kitty.go index 3d128a92f6..3023e636b7 100644 --- a/kitty.go +++ b/kitty.go @@ -7,59 +7,6 @@ import ( "github.com/charmbracelet/x/ansi" ) -// setKittyKeyboardFlagsMsg is a message to set Kitty keyboard progressive -// enhancement protocol flags. -type setKittyKeyboardFlagsMsg int - -// EnableKittyKeyboard is a command to enable Kitty keyboard progressive -// enhancements. -// -// The flags parameter is a bitmask of the following -// -// 1: Disambiguate escape codes -// 2: Report event types -// 4: Report alternate keys -// 8: Report all keys as escape codes -// 16: Report associated text -// -// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/ for more information. -func EnableKittyKeyboard(flags int) Cmd { //nolint:unused - return func() Msg { - return setKittyKeyboardFlagsMsg(flags) - } -} - -// DisableKittyKeyboard is a command to disable Kitty keyboard progressive -// enhancements. -func DisableKittyKeyboard() Msg { //nolint:unused - return setKittyKeyboardFlagsMsg(0) -} - -// kittyKeyboardMsg is a message that queries the current Kitty keyboard -// progressive enhancement flags. -type kittyKeyboardMsg struct{} - -// KittyKeyboard is a command that queries the current Kitty keyboard -// progressive enhancement flags from the terminal. -func KittyKeyboard() Msg { //nolint:unused - return kittyKeyboardMsg{} -} - -// KittyKeyboardMsg is a bitmask message representing Kitty keyboard -// progressive enhancement flags. -// -// The bitmaps represents the following: -// -// 0: Disable all features -// 1: Disambiguate escape codes -// 2: Report event types -// 4: Report alternate keys -// 8: Report all keys as escape codes -// 16: Report associated text -// -// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/ for more information. -type KittyKeyboardMsg int - // Kitty Clipboard Control Sequences var kittyKeyMap = map[int]rune{ ansi.BS: KeyBackspace, diff --git a/options.go b/options.go index c2f9f1e54d..e1aea30d91 100644 --- a/options.go +++ b/options.go @@ -253,72 +253,63 @@ func WithReportFocus() ProgramOption { } } -// WithEnhancedKeyboard enables support for enhanced keyboard features. This -// unambiguously reports more key combinations than traditional terminal -// keyboard sequences. This might also enable reporting of release key events -// depending on the terminal emulator supporting it. -// -// This is a syntactic sugar for WithKittyKeyboard(7) and WithXtermModifyOtherKeys(1). -func WithEnhancedKeyboard() ProgramOption { - return func(p *Program) { - _WithKittyKeyboard(ansi.KittyDisambiguateEscapeCodes | - ansi.KittyReportEventTypes | - ansi.KittyReportAlternateKeys, - )(p) - _WithModifyOtherKeys(1)(p) - } -} +// keyboardEnhancements is a type that represents a set of keyboard +// enhancements. +type keyboardEnhancements struct { + // Kitty progressive keyboard enhancements protocol. This can be used to + // enable different keyboard features. + // + // - 0: disable all features + // - 1: [ansi.DisambiguateEscapeCodes] Disambiguate escape codes such as + // ctrl+i and tab, ctrl+[ and escape, ctrl+space and ctrl+@, etc. + // - 2: [ansi.ReportEventTypes] Report event types such as key presses, + // releases, and repeat events. + // - 4: [ansi.ReportAlternateKeys] Report alternate keys such as shifted + // keys and PC-101 ANSI US keyboard layout. + // - 8: [ansi.ReportAllKeysAsEscapeCodes] Report all key events as escape + // codes. This includes simple printable keys like "a" and other Unicode + // characters. + // - 16: [ansi.ReportAssociatedText] Report associated text with key + // events. This encodes multi-rune key events as escape codes instead of + // individual runes. + // + kittyFlags int -// _WithKittyKeyboard enables support for the Kitty keyboard protocol. This -// protocol enables more key combinations and events than the traditional -// ambiguous terminal keyboard sequences. -// -// Use flags to specify which features you want to enable. -// -// 0: Disable all features -// 1: Disambiguate escape codes -// 2: Report event types -// 4: Report alternate keys -// 8: Report all keys as escape codes -// 16: Report associated text -// -// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/ for more information. -func _WithKittyKeyboard(flags int) ProgramOption { - return func(p *Program) { - p.kittyFlags = flags - p.startupOptions |= withKittyKeyboard - } + // Xterm modifyOtherKeys feature. + // + // - Mode 0 disables modifyOtherKeys. + // - Mode 1 reports ambiguous keys as escape codes. This is similar to + // [ansi.KittyDisambiguateEscapeCodes] but uses XTerm escape codes. + // - Mode 2 reports all key as escape codes including printable keys like "a" and "shift+b". + modifyOtherKeys int } -// _WithModifyOtherKeys enables support for the XTerm modifyOtherKeys feature. -// This feature allows the terminal to report ambiguous keys as escape codes. -// This is useful for terminals that don't support the Kitty keyboard protocol. -// -// The mode can be one of the following: -// -// 0: Disable modifyOtherKeys -// 1: Report ambiguous keys as escape codes -// 2: Report ambiguous keys as escape codes including modified keys like Alt- -// and Meta- +// KeyboardEnhancement is a type that represents a keyboard enhancement. +type KeyboardEnhancement func(k *keyboardEnhancements) + +// WithReleaseKeys enables support for reporting release key events. This is +// useful for terminals that support the Kitty keyboard protocol "Report event +// types" progressive enhancement feature. // -// See https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys -func _WithModifyOtherKeys(mode int) ProgramOption { - return func(p *Program) { - p.modifyOtherKeys = mode - p.startupOptions |= withModifyOtherKeys - } +// Note that not all terminals support this feature. +func WithReleaseKeys(k *keyboardEnhancements) { + k.kittyFlags |= ansi.KittyReportEventTypes } -// _WithWindowsInputMode enables Windows Input Mode (win32-input-mode) which -// allows for more advanced input handling and reporting. This is experimental -// and may not work on all terminals. +// WithKeyboardEnhancements enables support for enhanced keyboard features. This +// unambiguously reports more key combinations than traditional terminal +// keyboard sequences. This might also enable reporting of release key events +// depending on the terminal emulator supporting it. // -// See -// https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md -// for more information. -func _WithWindowsInputMode() ProgramOption { //nolint:unused +// This is a syntactic sugar for WithKittyKeyboard(1) and WithModifyOtherKeys(1). +func WithKeyboardEnhancements(enhancements ...KeyboardEnhancement) ProgramOption { + ke := keyboardEnhancements{kittyFlags: ansi.KittyDisambiguateEscapeCodes, modifyOtherKeys: 1} + for _, e := range enhancements { + e(&ke) + } return func(p *Program) { - p.startupOptions |= withWindowsInputMode + p.startupOptions |= withKeyboardEnhancements + p.keyboard = ke } } diff --git a/parse.go b/parse.go index 46e9db7e74..06c2581ee2 100644 --- a/parse.go +++ b/parse.go @@ -250,7 +250,7 @@ func parseCsi(b []byte) (int, Msg) { case 'u' | '?'<'<u\x1b[>3u\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[>0u", + cmds: []Cmd{DisableKeyboardEnhancements, EnableKeyboardEnhancements(WithReleaseKeys)}, + expected: "\x1b[?25l\x1b[?2004h\x1b[?2027h\x1b[?2027$p\x1b[>4;1m\x1b[>3u\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[>4;0m\x1b[>0u", }, } diff --git a/tea.go b/tea.go index bf58d831e2..35a5dadcb1 100644 --- a/tea.go +++ b/tea.go @@ -101,9 +101,7 @@ const ( withoutCatchPanics withoutBracketedPaste withReportFocus - withKittyKeyboard - withModifyOtherKeys - withWindowsInputMode + withKeyboardEnhancements withoutGraphemeClustering ) @@ -204,11 +202,7 @@ type Program struct { // rendererDone is used to stop the renderer. rendererDone chan struct{} - // kittyFlags stores kitty keyboard protocol progressive enhancement flags. - kittyFlags int - - // modifyOtherKeys stores the XTerm modifyOtherKeys mode. - modifyOtherKeys int + keyboard keyboardEnhancements // When a program is suspended, the terminal state is saved and the program // is paused. This saves the terminal colors state so they can be restored @@ -475,34 +469,41 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { case cursorColorMsg: p.execute(ansi.RequestCursorColor) - case KittyKeyboardMsg: - // Store the kitty flags whenever they are queried. - p.kittyFlags = int(msg) - - case setKittyKeyboardFlagsMsg: - p.kittyFlags = int(msg) - p.execute(ansi.PushKittyKeyboard(p.kittyFlags)) + case KeyboardEnhancementsMsg: + if msg.kittyFlags != p.keyboard.kittyFlags { + msg.kittyFlags |= p.keyboard.kittyFlags + } + if msg.modifyOtherKeys == 0 { + msg.modifyOtherKeys = p.keyboard.modifyOtherKeys + } - case kittyKeyboardMsg: - p.execute(ansi.RequestKittyKeyboard) + case enableKeyboardEnhancementsMsg: + var ke keyboardEnhancements + for _, e := range msg { + e(&ke) + } - case modifyOtherKeys: - p.execute(ansi.RequestModifyOtherKeys) + p.keyboard.kittyFlags |= ke.kittyFlags + if ke.modifyOtherKeys > p.keyboard.modifyOtherKeys { + p.keyboard.modifyOtherKeys = ke.modifyOtherKeys + } - case setModifyOtherKeysMsg: - p.modifyOtherKeys = int(msg) - p.execute(ansi.ModifyOtherKeys(p.modifyOtherKeys)) + if p.keyboard.modifyOtherKeys > 0 { + p.execute(ansi.ModifyOtherKeys(p.keyboard.modifyOtherKeys)) + } + if p.keyboard.kittyFlags > 0 { + p.execute(ansi.PushKittyKeyboard(p.keyboard.kittyFlags)) + } - case setEnhancedKeyboardMsg: - if bool(msg) { - p.kittyFlags = 3 - p.modifyOtherKeys = 1 - } else { - p.kittyFlags = 0 - p.modifyOtherKeys = 0 + case disableKeyboardEnhancementsMsg: + if p.keyboard.modifyOtherKeys > 0 { + p.execute(ansi.DisableModifyOtherKeys) + p.keyboard.modifyOtherKeys = 0 + } + if p.keyboard.kittyFlags > 0 { + p.execute(ansi.DisableKittyKeyboard) + p.keyboard.kittyFlags = 0 } - p.execute(ansi.ModifyOtherKeys(p.modifyOtherKeys)) - p.execute(ansi.PushKittyKeyboard(p.kittyFlags)) case execMsg: // NB: this blocks. @@ -705,20 +706,20 @@ func (p *Program) Run() (Model, error) { p.modes[ansi.MouseAllMotionMode] = true p.modes[ansi.MouseSgrExtMode] = true } - if p.startupOptions&withModifyOtherKeys != 0 { - p.execute(ansi.ModifyOtherKeys(p.modifyOtherKeys)) - } - if p.startupOptions&withKittyKeyboard != 0 { - p.execute(ansi.PushKittyKeyboard(p.kittyFlags)) - } if p.startupOptions&withReportFocus != 0 { p.execute(ansi.EnableReportFocus) p.modes[ansi.ReportFocusMode] = true } - if p.startupOptions&withWindowsInputMode != 0 { - p.execute(ansi.EnableWin32Input) - p.modes[ansi.Win32InputMode] = true + if p.startupOptions&withKeyboardEnhancements != 0 { + if p.keyboard.modifyOtherKeys > 0 { + p.execute(ansi.ModifyOtherKeys(p.keyboard.modifyOtherKeys)) + p.execute(ansi.RequestModifyOtherKeys) + } + if p.keyboard.kittyFlags > 0 { + p.execute(ansi.PushKittyKeyboard(p.keyboard.kittyFlags)) + p.execute(ansi.RequestKittyKeyboard) + } } // Start the renderer. @@ -913,11 +914,11 @@ func (p *Program) RestoreTerminal() error { if p.modes[ansi.BracketedPasteMode] { p.execute(ansi.EnableBracketedPaste) } - if p.modifyOtherKeys != 0 { - p.execute(ansi.ModifyOtherKeys(p.modifyOtherKeys)) + if p.keyboard.modifyOtherKeys != 0 { + p.execute(ansi.ModifyOtherKeys(p.keyboard.modifyOtherKeys)) } - if p.kittyFlags != 0 { - p.execute(ansi.PushKittyKeyboard(p.kittyFlags)) + if p.keyboard.kittyFlags != 0 { + p.execute(ansi.PushKittyKeyboard(p.keyboard.kittyFlags)) } if p.modes[ansi.ReportFocusMode] { p.execute(ansi.EnableReportFocus) diff --git a/tty.go b/tty.go index 464d7a4eec..97b987321b 100644 --- a/tty.go +++ b/tty.go @@ -71,10 +71,10 @@ func (p *Program) restoreTerminalState() error { p.execute(ansi.DisableMouseAllMotion) p.execute(ansi.DisableMouseSgrExt) } - if p.modifyOtherKeys != 0 { + if p.keyboard.modifyOtherKeys != 0 { p.execute(ansi.DisableModifyOtherKeys) } - if p.kittyFlags != 0 { + if p.keyboard.kittyFlags != 0 { p.execute(ansi.DisableKittyKeyboard) } if p.modes[ansi.ReportFocusMode] { diff --git a/xterm.go b/xterm.go index dc7efe9f69..b6140b5eab 100644 --- a/xterm.go +++ b/xterm.go @@ -4,29 +4,6 @@ import ( "github.com/charmbracelet/x/ansi" ) -// setModifyOtherKeysMsg is a message to set XTerm modifyOtherKeys mode. -type setModifyOtherKeysMsg int - -// EnableXtermModifyOtherKeys is a command to enable XTerm modifyOtherKeys mode. -// -// The mode can be on of the following: -// -// 1: Report ambiguous keys as escape codes -// 2: Report ambiguous keys as escape codes including modified keys like Alt- -// and Meta- -// -// See https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys -func EnableModifyOtherKeys(mode int) Cmd { //nolint:unused - return func() Msg { - return setModifyOtherKeysMsg(mode) - } -} - -// DisableModifyOtherKeys is a command to disable XTerm modifyOtherKeys mode. -func DisableModifyOtherKeys() Msg { //nolint:unused - return setModifyOtherKeysMsg(0) -} - func parseXTermModifyOtherKeys(csi *ansi.CsiSequence) Msg { // XTerm modify other keys starts with ESC [ 27 ; ; ~ mod := KeyMod(csi.Param(1) - 1) @@ -54,28 +31,6 @@ func parseXTermModifyOtherKeys(csi *ansi.CsiSequence) Msg { return k } -// modifyOtherKeys is an internal message that queries the terminal for its -// modifyOtherKeys mode. -type modifyOtherKeys struct{} - -// ModifyOtherKeys is a command that queries the terminal for its -// modifyOtherKeys mode. -func ModifyOtherKeys() Msg { //nolint:unused - return modifyOtherKeys{} -} - -// ModifyOtherKeysMsg is a message that represents XTerm modifyOtherKeys -// report. Querying the terminal for the modifyOtherKeys mode will return a -// ModifyOtherKeysMsg message with the current mode set. -// -// 0: disable -// 1: enable mode 1 -// 2: enable mode 2 -// -// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ -// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys -type ModifyOtherKeysMsg int - // TerminalVersionMsg is a message that represents the terminal version. type TerminalVersionMsg string