Skip to content

Conversation

@metaspartan
Copy link
Owner

@metaspartan metaspartan commented Dec 12, 2025

Note

Introduce a multi-instance Backend with CustomTTY (SSH-ready), context-aware event polling, updated render/clear APIs, and a new SSH dashboard example with docs; bump deps.

  • Core/Backend:
    • Add Backend type with DefaultBackend and config-driven InitWithConfig (CustomTTY, SimulationMode, size).
    • Introduce TTYHandle and ttyAdapter to bind custom I/O (e.g., SSH) to tcell.
    • Provide global wrappers (Init, Close, Render, Clear, etc.) delegating to DefaultBackend; keep legacy Screen/ScreenshotMode vars.
  • Events:
    • Add Backend.PollEvents and PollEventsWithContext(ctx); implement context-cancellable polling via tcell.EventInterrupt.
  • Rendering:
    • Convert Render to instance method (Backend.Render); respect per-backend ScreenshotMode.
  • Examples:
    • New _examples/ssh-dashboard/ showcasing multi-user SSH TUI using gliderlabs/ssh and per-session backends.
  • Docs:
    • README: advertise SSH/remote apps, add "Serving over SSH" guide, and list SSH Dashboard in gallery.
  • Dependencies:
    • Bump tcell to v2.13.4, go-runewidth to v0.0.19; add golang.org/x/image; new indirect uax29.
  • Repo:
    • Update .gitignore for SSH host keys and artifacts.

Written by Cursor Bugbot for commit 9e6c0eb. This will update automatically on new commits. Configure here.

17twenty and others added 8 commits December 12, 2025 13:58
Add SSH/multi-session support without exposing tcell types.

- InitWithConfig() accepts io.ReadWriter for custom TTY
- PollEventsWithContext() fixes goroutine leaks
- Fully backward compatible
- Simplifies SSH example from 28 to 10 lines
Refactored core rendering and event logic to support multiple independent Backend instances, enabling true multi-user and SSH session support. Updated the SSH dashboard example to use per-session backends, removed global state reliance, and improved documentation for SSH usage. Added new dependencies for SSH support and updated .gitignore for SSH-related files.
Updated Init and InitWithConfig to assign Screen and ScreenshotMode from DefaultBackend after initialization. Also added a nil check for Screen in Backend.Render to prevent rendering when the screen is uninitialized. Updated dependencies in go.mod and go.sum.
InitWithConfig now checks if the provided config is nil and falls back to the default Init method. This prevents potential nil pointer dereference errors when no configuration is supplied.
Add SSH/multi-session support without exposing tcell types.
Copilot AI review requested due to automatic review settings December 12, 2025 04:39
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR (v4.1.9) introduces a significant architectural refactoring to support multi-tenant TUI applications, particularly for SSH-based deployments. The main change is transitioning from global state (Screen variable) to instance-based Backend objects, enabling multiple isolated terminal sessions to run concurrently.

Key changes:

  • Refactored Backend architecture to support instance-based screens instead of global state
  • Added context-aware event polling with PollEventsWithContext for graceful cancellation
  • Introduced SSH/custom TTY support through InitConfig and TTYHandle interface
  • Updated dependencies (tcell v2.13.4, go-runewidth v0.0.19, golang.org/x packages)

Reviewed changes

Copilot reviewed 7 out of 9 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
backend.go Major refactoring: introduces Backend struct, InitConfig for custom TTY, and ttyAdapter for SSH support
render.go Converts Render function to Backend method while maintaining global compatibility
events.go Adds Backend.PollEvents methods and context-aware PollEventsWithContext for proper cleanup
go.mod Updates dependencies and Go version to 1.24.0
go.sum Reflects updated dependency checksums
_examples/ssh-dashboard/main.go New complete example demonstrating multi-user SSH dashboard
_examples/ssh-dashboard/README.md Documentation for the SSH dashboard example
README.md Adds SSH/remote apps feature documentation and usage examples
.gitignore Adds SSH-related files to ignore list
Comments suppressed due to low confidence (1)

render.go:61

  • Lines 59-61 show dead code where width and height are calculated but immediately overwritten. The calculation on line 59 (1024/7, 768/13) appears to be leftover code that should be removed for clarity.
		width, height := 1024/7, 768/13 // approx 146x59
		// Or 120x40
		width, height = 120, 60

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

func newSessionTTY(sess ssh.Session) (*sessionTTY, error) {
pty, winCh, ok := sess.Pty()
if !ok {
return nil, fmt.Errorf("no PTY requested (try: ssh -tt host -p 2222)")
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message suggests using "ssh -tt host -p 2222" but 'host' is not a valid hostname. It should suggest a concrete example like "ssh -tt localhost -p 2222" or "ssh -tt 0.0.0.0 -p 2222" to match the server address.

Suggested change
return nil, fmt.Errorf("no PTY requested (try: ssh -tt host -p 2222)")
return nil, fmt.Errorf("no PTY requested (try: ssh -tt localhost -p 2222)")

Copilot uses AI. Check for mistakes.

```bash
$ ssh-keygen -t ed25519 -f hostkey -N "" # Generate sample host key
$ go run _examples/stacked_barchart/main.go
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The command references the wrong example path. It should reference ssh-dashboard/main.go instead of stacked_barchart/main.go.

Suggested change
$ go run _examples/stacked_barchart/main.go
$ go run _examples/ssh-dashboard/main.go

Copilot uses AI. Check for mistakes.
| **Radarchart** | <img src="_examples/radarchart/screenshot.png" height="80" /> | [View Example Code](_examples/radarchart/main.go) |
| **Scrollbar** | <img src="_examples/scrollbar/screenshot.png" height="80" /> | [View Example Code](_examples/scrollbar/main.go) |
| **Sparkline** | <img src="_examples/sparkline/screenshot.png" height="80" /> | [View Example Code](_examples/sparkline/main.go) |
| **SSH Dashboard** | <img src="_examples/dashboard/screenshot.png" height="80" /> | [View Example Code](_examples/ssh-dashboard/main.go) |
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image reference points to _examples/dashboard/screenshot.png but this is for the SSH Dashboard example. The path should likely be _examples/ssh-dashboard/screenshot.png to match the example's actual directory name.

Suggested change
| **SSH Dashboard** | <img src="_examples/dashboard/screenshot.png" height="80" /> | [View Example Code](_examples/ssh-dashboard/main.go) |
| **SSH Dashboard** | <img src="_examples/ssh-dashboard/screenshot.png" height="80" /> | [View Example Code](_examples/ssh-dashboard/main.go) |

Copilot uses AI. Check for mistakes.
Comment on lines 102 to 106
for i, arg := range os.Args {
if arg == "-screenshot" {
ScreenshotMode = true
b.ScreenshotMode = true
// Remove flag so app logic doesn't see it
os.Args = append(os.Args[:i], os.Args[i+1:]...)
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modifying os.Args during iteration can lead to incorrect behavior if multiple backends are initialized or if the flag appears more than once. After removing an element at index i, the loop continues with i+1 but the slice has shifted. Consider breaking after finding and removing the flag, or iterating backwards.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,20 @@
# SSH Dashboard Example

This example demonstrates the **SSH Dashboard** widget/feature.
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "SSH Dashboard widget/feature" but this is not a widget, it's a complete example application demonstrating how to use gotui over SSH. Consider changing to "This example demonstrates serving a gotui dashboard over SSH" or similar to avoid confusion.

Suggested change
This example demonstrates the **SSH Dashboard** widget/feature.
This example demonstrates serving a gotui dashboard over SSH.

Copilot uses AI. Check for mistakes.
Comment on lines +185 to +187
app, _ := ui.NewBackend(&ui.InitConfig{
CustomTTY: sess, // ssh.Session implements io.ReadWriter
})
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code example shows error handling with _ (ignored errors) which is not a good practice to demonstrate in documentation. Consider showing proper error handling with return or log.Fatal to set a better example for users.

Suggested change
app, _ := ui.NewBackend(&ui.InitConfig{
CustomTTY: sess, // ssh.Session implements io.ReadWriter
})
app, err := ui.NewBackend(&ui.InitConfig{
CustomTTY: sess, // ssh.Session implements io.ReadWriter
})
if err != nil {
// handle error appropriately, e.g., log or return
return
}

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +62
go func() {
for {
select {
case <-t.closed:
return
case win, ok := <-t.winCh:
if !ok {
return
}
t.mu.Lock()
t.w, t.h = win.Width, win.Height
cb := t.resizeCb
t.mu.Unlock()
if cb != nil {
cb()
}
}
}
}()
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goroutine started in newSessionTTY (lines 44-62) can leak if winCh is never closed and the closed channel is not signaled. While the Close method does close the channel, if Close is not called (e.g., if newSessionTTY returns an error after creating the channel but before returning), the goroutine will leak. Consider starting the goroutine after all initialization is complete and the sessionTTY is ready to be returned, or ensure the closed channel is always properly signaled even in error paths.

Copilot uses AI. Check for mistakes.
@metaspartan metaspartan merged commit 9e6c0eb into master Dec 12, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants