From 1f88b9ed6e9cdec8f990ba8ac33b0546badca790 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 20 Aug 2024 17:10:21 -0400 Subject: [PATCH] refactor: simplify renderer interface Use Render instead of Write/WriteString --- nil_renderer.go | 57 +++++++++++++++++------- renderer.go | 12 +---- screen.go | 30 +++---------- standard_renderer.go | 47 +++++++++----------- tea.go | 101 +++++++++++++++++++++++-------------------- tty.go | 6 +-- 6 files changed, 125 insertions(+), 128 deletions(-) diff --git a/nil_renderer.go b/nil_renderer.go index 45046b6ff4..c9f1770f19 100644 --- a/nil_renderer.go +++ b/nil_renderer.go @@ -8,19 +8,44 @@ type NilRenderer struct{} var _ Renderer = NilRenderer{} -func (NilRenderer) SetOutput(io.Writer) {} -func (NilRenderer) Flush() error { return nil } -func (NilRenderer) Close() error { return nil } -func (NilRenderer) Write([]byte) (int, error) { return 0, nil } -func (NilRenderer) WriteString(string) (int, error) { return 0, nil } -func (NilRenderer) Repaint() {} -func (NilRenderer) ClearScreen() {} -func (NilRenderer) AltScreen() bool { return false } -func (NilRenderer) EnterAltScreen() {} -func (NilRenderer) ExitAltScreen() {} -func (NilRenderer) CursorVisibility() bool { return false } -func (NilRenderer) ShowCursor() {} -func (NilRenderer) HideCursor() {} -func (NilRenderer) Execute(string) {} -func (NilRenderer) InsertAbove(string) error { return nil } -func (NilRenderer) Resize(int, int) {} +// SetOutput implements the Renderer interface. +func (NilRenderer) SetOutput(io.Writer) {} + +// Flush implements the Renderer interface. +func (NilRenderer) Flush() error { return nil } + +// Close implements the Renderer interface. +func (NilRenderer) Close() error { return nil } + +// Render implements the Renderer interface. +func (NilRenderer) Render(string) error { return nil } + +// Repaint implements the Renderer interface. +func (NilRenderer) Repaint() {} + +// ClearScreen implements the Renderer interface. +func (NilRenderer) ClearScreen() {} + +// AltScreen implements the Renderer interface. +func (NilRenderer) AltScreen() bool { return false } + +// EnterAltScreen implements the Renderer interface. +func (NilRenderer) EnterAltScreen() {} + +// ExitAltScreen implements the Renderer interface. +func (NilRenderer) ExitAltScreen() {} + +// CursorVisibility implements the Renderer interface. +func (NilRenderer) CursorVisibility() bool { return false } + +// ShowCursor implements the Renderer interface. +func (NilRenderer) ShowCursor() {} + +// HideCursor implements the Renderer interface. +func (NilRenderer) HideCursor() {} + +// InsertAbove implements the Renderer interface. +func (NilRenderer) InsertAbove(string) error { return nil } + +// Resize implements the Renderer interface. +func (NilRenderer) Resize(int, int) {} diff --git a/renderer.go b/renderer.go index 03aa2a05e0..44da92a61d 100644 --- a/renderer.go +++ b/renderer.go @@ -7,13 +7,8 @@ type Renderer interface { // Close closes the renderer and flushes any remaining data. Close() error - // Write a frame to the renderer. The renderer can write this data to - // output at its discretion. - Write([]byte) (int, error) - - // WriteString a frame to the renderer. The renderer can WriteString this - // data to output at its discretion. - WriteString(string) (int, error) + // Render renders a frame to the output. + Render(string) error // SetOutput sets the output for the renderer. SetOutput(io.Writer) @@ -50,9 +45,6 @@ type Renderer interface { ShowCursor() // Hide the cursor. HideCursor() - - // Execute writes a sequence to the underlying output. - Execute(string) } // repaintMsg forces a full repaint. diff --git a/screen.go b/screen.go index 22e440cefe..61898c4c93 100644 --- a/screen.go +++ b/screen.go @@ -190,11 +190,7 @@ func (p *Program) ExitAltScreen() { // // Deprecated: Use the WithMouseCellMotion ProgramOption instead. func (p *Program) EnableMouseCellMotion() { - if p.renderer != nil { - p.renderer.Execute(ansi.EnableMouseCellMotion) - } else { - p.startupOptions |= withMouseCellMotion - } + p.execute(ansi.EnableMouseCellMotion) } // DisableMouseCellMotion disables Mouse Cell Motion tracking. This will be @@ -202,11 +198,7 @@ func (p *Program) EnableMouseCellMotion() { // // Deprecated: The mouse will automatically be disabled when the program exits. func (p *Program) DisableMouseCellMotion() { - if p.renderer != nil { - p.renderer.Execute(ansi.DisableMouseCellMotion) - } else { - p.startupOptions &^= withMouseCellMotion - } + p.execute(ansi.DisableMouseCellMotion) } // EnableMouseAllMotion enables mouse click, release, wheel and motion events, @@ -215,11 +207,7 @@ func (p *Program) DisableMouseCellMotion() { // // Deprecated: Use the WithMouseAllMotion ProgramOption instead. func (p *Program) EnableMouseAllMotion() { - if p.renderer != nil { - p.renderer.Execute(ansi.EnableMouseAllMotion) - } else { - p.startupOptions |= withMouseAllMotion - } + p.execute(ansi.EnableMouseAllMotion) } // DisableMouseAllMotion disables All Motion mouse tracking. This will be @@ -227,20 +215,12 @@ func (p *Program) EnableMouseAllMotion() { // // Deprecated: The mouse will automatically be disabled when the program exits. func (p *Program) DisableMouseAllMotion() { - if p.renderer != nil { - p.renderer.Execute(ansi.DisableMouseAllMotion) - } else { - p.startupOptions &^= withMouseAllMotion - } + p.execute(ansi.DisableMouseAllMotion) } // SetWindowTitle sets the terminal window title. // // Deprecated: Use the SetWindowTitle command instead. func (p *Program) SetWindowTitle(title string) { - if p.renderer != nil { - p.renderer.Execute(ansi.SetWindowTitle(title)) - } else { - p.startupTitle = title - } + p.execute(ansi.SetWindowTitle(title)) } diff --git a/standard_renderer.go b/standard_renderer.go index 95cd14021e..304f40a212 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -67,15 +67,15 @@ func (r *standardRenderer) Close() (err error) { r.mtx.Lock() defer r.mtx.Unlock() - r.Execute(ansi.EraseEntireLine) + r.execute(ansi.EraseEntireLine) // Move the cursor back to the beginning of the line - r.Execute("\r") + r.execute("\r") return } -// Execute writes a sequence to the terminal. -func (r *standardRenderer) Execute(seq string) { +// execute writes the given sequence to the output. +func (r *standardRenderer) execute(seq string) { _, _ = io.WriteString(r.out, seq) } @@ -211,9 +211,9 @@ func (r *standardRenderer) Flush() (err error) { return } -// WriteString writes to the internal buffer. The buffer will be outputted via the -// ticker which calls flush(). -func (r *standardRenderer) WriteString(s string) (int, error) { +// Render renders the frame to the internal buffer. The buffer will be +// outputted via the ticker which calls flush(). +func (r *standardRenderer) Render(s string) error { r.mtx.Lock() defer r.mtx.Unlock() r.buf.Reset() @@ -226,13 +226,8 @@ func (r *standardRenderer) WriteString(s string) (int, error) { s = " " } - return r.buf.WriteString(s) -} - -// Write writes to the internal buffer. The buffer will be outputted via the -// ticker which calls flush(). -func (r *standardRenderer) Write(p []byte) (int, error) { - return r.WriteString(string(p)) + _, err := r.buf.WriteString(s) + return err } // Repaint forces a full repaint. @@ -244,8 +239,8 @@ func (r *standardRenderer) ClearScreen() { r.mtx.Lock() defer r.mtx.Unlock() - r.Execute(ansi.EraseEntireDisplay) - r.Execute(ansi.MoveCursorOrigin) + r.execute(ansi.EraseEntireDisplay) + r.execute(ansi.MoveCursorOrigin) r.Repaint() } @@ -266,7 +261,7 @@ func (r *standardRenderer) EnterAltScreen() { } r.altScreenActive = true - r.Execute(ansi.EnableAltScreenBuffer) + r.execute(ansi.EnableAltScreenBuffer) // Ensure that the terminal is cleared, even when it doesn't support // alt screen (or alt screen support is disabled, like GNU screen by @@ -274,16 +269,16 @@ func (r *standardRenderer) EnterAltScreen() { // // Note: we can't use r.clearScreen() here because the mutex is already // locked. - r.Execute(ansi.EraseEntireDisplay) - r.Execute(ansi.MoveCursorOrigin) + r.execute(ansi.EraseEntireDisplay) + r.execute(ansi.MoveCursorOrigin) // cmd.exe and other terminals keep separate cursor states for the AltScreen // and the main buffer. We have to explicitly reset the cursor visibility // whenever we enter AltScreen. if r.cursorHidden { - r.Execute(ansi.HideCursor) + r.execute(ansi.HideCursor) } else { - r.Execute(ansi.ShowCursor) + r.execute(ansi.ShowCursor) } r.Repaint() @@ -298,15 +293,15 @@ func (r *standardRenderer) ExitAltScreen() { } r.altScreenActive = false - r.Execute(ansi.DisableAltScreenBuffer) + r.execute(ansi.DisableAltScreenBuffer) // cmd.exe and other terminals keep separate cursor states for the AltScreen // and the main buffer. We have to explicitly reset the cursor visibility // whenever we exit AltScreen. if r.cursorHidden { - r.Execute(ansi.HideCursor) + r.execute(ansi.HideCursor) } else { - r.Execute(ansi.ShowCursor) + r.execute(ansi.ShowCursor) } r.Repaint() @@ -321,7 +316,7 @@ func (r *standardRenderer) ShowCursor() { defer r.mtx.Unlock() r.cursorHidden = false - r.Execute(ansi.ShowCursor) + r.execute(ansi.ShowCursor) } func (r *standardRenderer) HideCursor() { @@ -329,7 +324,7 @@ func (r *standardRenderer) HideCursor() { defer r.mtx.Unlock() r.cursorHidden = true - r.Execute(ansi.HideCursor) + r.execute(ansi.HideCursor) } // setIgnoredLines specifies lines not to be touched by the standard Bubble Tea diff --git a/tea.go b/tea.go index fcbfaf0098..3e3a292304 100644 --- a/tea.go +++ b/tea.go @@ -349,9 +349,9 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} { } func (p *Program) disableMouse() { - p.renderer.Execute(ansi.DisableMouseCellMotion) - p.renderer.Execute(ansi.DisableMouseAllMotion) - p.renderer.Execute(ansi.DisableMouseSgrExt) + p.execute(ansi.DisableMouseCellMotion) + p.execute(ansi.DisableMouseAllMotion) + p.execute(ansi.DisableMouseSgrExt) } // eventLoop is the central message loop. It receives and handles the default @@ -396,12 +396,12 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { case enableMouseCellMotionMsg, enableMouseAllMotionMsg: switch msg.(type) { case enableMouseCellMotionMsg: - p.renderer.Execute(ansi.EnableMouseCellMotion) + p.execute(ansi.EnableMouseCellMotion) case enableMouseAllMotionMsg: - p.renderer.Execute(ansi.EnableMouseAllMotion) + p.execute(ansi.EnableMouseAllMotion) } // mouse mode (1006) is a no-op if the terminal doesn't support it. - p.renderer.Execute(ansi.EnableMouseSgrExt) + p.execute(ansi.EnableMouseSgrExt) case disableMouseMsg: p.disableMouse() @@ -413,54 +413,54 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { p.renderer.HideCursor() case enableBracketedPasteMsg: - p.renderer.Execute(ansi.EnableBracketedPaste) + p.execute(ansi.EnableBracketedPaste) p.bpActive = true case disableBracketedPasteMsg: - p.renderer.Execute(ansi.DisableBracketedPaste) + p.execute(ansi.DisableBracketedPaste) p.bpActive = false case enableReportFocusMsg: - p.renderer.Execute(ansi.EnableReportFocus) + p.execute(ansi.EnableReportFocus) case disableReportFocusMsg: - p.renderer.Execute(ansi.DisableReportFocus) + p.execute(ansi.DisableReportFocus) case readClipboardMsg: - p.renderer.Execute(ansi.RequestSystemClipboard) + p.execute(ansi.RequestSystemClipboard) case setClipboardMsg: - p.renderer.Execute(ansi.SetSystemClipboard(string(msg))) + p.execute(ansi.SetSystemClipboard(string(msg))) case readPrimaryClipboardMsg: - p.renderer.Execute(ansi.RequestPrimaryClipboard) + p.execute(ansi.RequestPrimaryClipboard) case setPrimaryClipboardMsg: - p.renderer.Execute(ansi.SetPrimaryClipboard(string(msg))) + p.execute(ansi.SetPrimaryClipboard(string(msg))) case setBackgroundColorMsg: if msg.Color != nil { - p.renderer.Execute(ansi.SetBackgroundColor(msg.Color)) + p.execute(ansi.SetBackgroundColor(msg.Color)) } case setForegroundColorMsg: if msg.Color != nil { - p.renderer.Execute(ansi.SetForegroundColor(msg.Color)) + p.execute(ansi.SetForegroundColor(msg.Color)) } case setCursorColorMsg: if msg.Color != nil { - p.renderer.Execute(ansi.SetCursorColor(msg.Color)) + p.execute(ansi.SetCursorColor(msg.Color)) } case backgroundColorMsg: - p.renderer.Execute(ansi.RequestBackgroundColor) + p.execute(ansi.RequestBackgroundColor) case foregroundColorMsg: - p.renderer.Execute(ansi.RequestForegroundColor) + p.execute(ansi.RequestForegroundColor) case cursorColorMsg: - p.renderer.Execute(ansi.RequestCursorColor) + p.execute(ansi.RequestCursorColor) case _KittyKeyboardMsg: // Store the kitty flags whenever they are queried. @@ -468,17 +468,17 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { case setKittyKeyboardFlagsMsg: p.kittyFlags = int(msg) - p.renderer.Execute(ansi.PushKittyKeyboard(p.kittyFlags)) + p.execute(ansi.PushKittyKeyboard(p.kittyFlags)) case kittyKeyboardMsg: - p.renderer.Execute(ansi.RequestKittyKeyboard) + p.execute(ansi.RequestKittyKeyboard) case modifyOtherKeys: - p.renderer.Execute(ansi.RequestModifyOtherKeys) + p.execute(ansi.RequestModifyOtherKeys) case setModifyOtherKeysMsg: p.modifyOtherKeys = int(msg) - p.renderer.Execute(ansi.ModifyOtherKeys(p.modifyOtherKeys)) + p.execute(ansi.ModifyOtherKeys(p.modifyOtherKeys)) case setEnhancedKeyboardMsg: if bool(msg) { @@ -488,15 +488,15 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { p.kittyFlags = 0 p.modifyOtherKeys = 0 } - p.renderer.Execute(ansi.ModifyOtherKeys(p.modifyOtherKeys)) - p.renderer.Execute(ansi.PushKittyKeyboard(p.kittyFlags)) + p.execute(ansi.ModifyOtherKeys(p.modifyOtherKeys)) + p.execute(ansi.PushKittyKeyboard(p.kittyFlags)) case enableWin32InputMsg: - p.renderer.Execute(ansi.EnableWin32Input) + p.execute(ansi.EnableWin32Input) p.win32Input = true case disableWin32InputMsg: - p.renderer.Execute(ansi.DisableWin32Input) + p.execute(ansi.DisableWin32Input) p.win32Input = false case execMsg: @@ -504,10 +504,10 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { p.exec(msg.cmd, msg.fn) case terminalVersion: - p.renderer.Execute(ansi.RequestXTVersion) + p.execute(ansi.RequestXTVersion) case primaryDeviceAttrsMsg: - p.renderer.Execute(ansi.RequestPrimaryDeviceAttributes) + p.execute(ansi.RequestPrimaryDeviceAttributes) case BatchMsg: for _, cmd := range msg { @@ -562,9 +562,9 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { } var cmd Cmd - model, cmd = model.Update(msg) // run update - cmds <- cmd // process command (if any) - p.renderer.WriteString(model.View()) //nolint:errcheck // send view to renderer + model, cmd = model.Update(msg) // run update + cmds <- cmd // process command (if any) + p.renderer.Render(model.View()) //nolint:errcheck // send view to renderer } } } @@ -682,34 +682,34 @@ func (p *Program) Run() (Model, error) { // Honor program startup options. if p.startupTitle != "" { - p.renderer.Execute(ansi.SetWindowTitle(p.startupTitle)) + p.execute(ansi.SetWindowTitle(p.startupTitle)) } if p.startupOptions&withAltScreen != 0 { p.renderer.EnterAltScreen() } if p.startupOptions&withoutBracketedPaste == 0 { - p.renderer.Execute(ansi.EnableBracketedPaste) + p.execute(ansi.EnableBracketedPaste) p.bpActive = true } if p.startupOptions&withMouseCellMotion != 0 { - p.renderer.Execute(ansi.EnableMouseCellMotion) - p.renderer.Execute(ansi.EnableMouseSgrExt) + p.execute(ansi.EnableMouseCellMotion) + p.execute(ansi.EnableMouseSgrExt) } else if p.startupOptions&withMouseAllMotion != 0 { - p.renderer.Execute(ansi.EnableMouseAllMotion) - p.renderer.Execute(ansi.EnableMouseSgrExt) + p.execute(ansi.EnableMouseAllMotion) + p.execute(ansi.EnableMouseSgrExt) } if p.startupOptions&withModifyOtherKeys != 0 { - p.renderer.Execute(ansi.ModifyOtherKeys(p.modifyOtherKeys)) + p.execute(ansi.ModifyOtherKeys(p.modifyOtherKeys)) } if p.startupOptions&withKittyKeyboard != 0 { - p.renderer.Execute(ansi.PushKittyKeyboard(p.kittyFlags)) + p.execute(ansi.PushKittyKeyboard(p.kittyFlags)) } if p.startupOptions&withReportFocus != 0 { - p.renderer.Execute(ansi.EnableReportFocus) + p.execute(ansi.EnableReportFocus) } if p.startupOptions&withWindowsInputMode != 0 { - p.renderer.Execute(ansi.EnableWin32Input) + p.execute(ansi.EnableWin32Input) } // Start the renderer. @@ -731,7 +731,7 @@ func (p *Program) Run() (Model, error) { } // Render the initial view. - p.renderer.WriteString(model.View()) //nolint:errcheck + p.renderer.Render(model.View()) //nolint:errcheck // Handle resize events. handlers.add(p.handleResize()) @@ -746,7 +746,7 @@ func (p *Program) Run() (Model, error) { err = fmt.Errorf("%w: %s", ErrProgramKilled, p.ctx.Err()) } else { // Ensure we rendered the final state of the model. - p.renderer.WriteString(model.View()) //nolint:errcheck + p.renderer.Render(model.View()) //nolint:errcheck } // Tear down. @@ -826,6 +826,11 @@ func (p *Program) Wait() { <-p.finished } +// execute writes the given sequence to the program output. +func (p *Program) execute(seq string) { + io.WriteString(p.output, seq) //nolint:errcheck +} + // shutdown performs operations to free up resources and restore the terminal // to its original state. func (p *Program) shutdown(kill bool) { @@ -876,13 +881,13 @@ func (p *Program) RestoreTerminal() error { if p.renderer != nil { p.startRenderer() p.renderer.HideCursor() - p.renderer.Execute(ansi.EnableBracketedPaste) + p.execute(ansi.EnableBracketedPaste) p.bpActive = true if p.modifyOtherKeys != 0 { - p.renderer.Execute(ansi.ModifyOtherKeys(p.modifyOtherKeys)) + p.execute(ansi.ModifyOtherKeys(p.modifyOtherKeys)) } if p.kittyFlags != 0 { - p.renderer.Execute(ansi.PushKittyKeyboard(p.kittyFlags)) + p.execute(ansi.PushKittyKeyboard(p.kittyFlags)) } } diff --git a/tty.go b/tty.go index a74ca8dffb..4ec49bcdd0 100644 --- a/tty.go +++ b/tty.go @@ -33,15 +33,15 @@ func (p *Program) initTerminal() error { // Bubble Tea program. func (p *Program) restoreTerminalState() error { if p.renderer != nil { - p.renderer.Execute(ansi.DisableBracketedPaste) + p.execute(ansi.DisableBracketedPaste) p.bpActive = false p.renderer.ShowCursor() p.disableMouse() if p.modifyOtherKeys != 0 { - p.renderer.Execute(ansi.DisableModifyOtherKeys) + p.execute(ansi.DisableModifyOtherKeys) } if p.kittyFlags != 0 { - p.renderer.Execute(ansi.DisableKittyKeyboard) + p.execute(ansi.DisableKittyKeyboard) } if p.renderer.AltScreen() {