chore: add cursor support

Kujtim Hoxha created

Change summary

internal/terminal/terminal.go                            | 35 +++++++--
internal/tui/components/dialogs/lazygit/lazygit.go       | 22 +++---
internal/tui/components/dialogs/termdialog/termdialog.go | 21 ++++++
3 files changed, 59 insertions(+), 19 deletions(-)

Detailed changes

internal/terminal/terminal.go 🔗

@@ -53,10 +53,11 @@ type Terminal struct {
 	vterm *vt.Emulator
 	cmd   *exec.Cmd
 
-	width       int
-	height      int
-	mouseMode   uv.MouseMode
-	refreshRate time.Duration
+	width         int
+	height        int
+	mouseMode     uv.MouseMode
+	cursorVisible bool
+	refreshRate   time.Duration
 
 	started bool
 	closed  bool
@@ -84,9 +85,10 @@ func New(cfg Config) *Terminal {
 	}
 
 	return &Terminal{
-		ctx:         ctx,
-		cmd:         cmd,
-		refreshRate: refreshRate,
+		ctx:           ctx,
+		cmd:           cmd,
+		refreshRate:   refreshRate,
+		cursorVisible: true, // Cursor is visible by default
 	}
 }
 
@@ -150,7 +152,7 @@ func (t *Terminal) Start() error {
 	return nil
 }
 
-// setupCallbacks configures vterm callbacks to track mouse mode.
+// setupCallbacks configures vterm callbacks to track mouse mode and cursor visibility.
 func (t *Terminal) setupCallbacks() {
 	t.vterm.SetCallbacks(vt.Callbacks{
 		EnableMode: func(mode ansi.Mode) {
@@ -169,6 +171,9 @@ func (t *Terminal) setupCallbacks() {
 				t.mouseMode = uv.MouseModeNone
 			}
 		},
+		CursorVisibility: func(visible bool) {
+			t.cursorVisible = visible
+		},
 	})
 }
 
@@ -265,6 +270,20 @@ func (t *Terminal) Render() string {
 	return t.vterm.Render()
 }
 
+// CursorPosition returns the current cursor position in the terminal.
+// Returns (-1, -1) if the terminal is not started, closed, or cursor is hidden.
+func (t *Terminal) CursorPosition() (x, y int) {
+	t.mu.RLock()
+	defer t.mu.RUnlock()
+
+	if t.vterm == nil || !t.started || t.closed || !t.cursorVisible {
+		return -1, -1
+	}
+
+	pos := t.vterm.CursorPosition()
+	return pos.X, pos.Y
+}
+
 // Started returns whether the terminal has been started.
 func (t *Terminal) Started() bool {
 	t.mu.RLock()

internal/tui/components/dialogs/lazygit/lazygit.go 🔗

@@ -93,17 +93,17 @@ func createThemedConfig() string {
     defaultFgColor:
       - default
 `,
-		colorToHex(t.BorderFocus),       // Active border: purple (Charple)
-		colorToHex(t.Border),            // Inactive border: gray (Charcoal)
-		colorToHex(t.Info),              // Search border: blue (Malibu) - calmer than warning
-		colorToHex(t.FgMuted),           // Options text: muted gray (Squid) - matches help text
-		colorToHex(t.Primary),           // Selected line bg: purple (Charple) - matches TextSelected
-		colorToHex(t.BgSubtle),          // Inactive selected: subtle gray (Charcoal)
-		colorToHex(t.Success),           // Cherry-picked fg: green (Guac) - positive action
-		colorToHex(t.BgSubtle),          // Cherry-picked bg: subtle (Charcoal)
-		colorToHex(t.Info),              // Marked base fg: blue (Malibu) - distinct from cherry
-		colorToHex(t.BgSubtle),          // Marked base bg: subtle (Charcoal)
-		colorToHex(t.Error),             // Unstaged changes: red (Sriracha)
+		colorToHex(t.BorderFocus), // Active border: purple (Charple)
+		colorToHex(t.Border),      // Inactive border: gray (Charcoal)
+		colorToHex(t.Info),        // Search border: blue (Malibu) - calmer than warning
+		colorToHex(t.FgMuted),     // Options text: muted gray (Squid) - matches help text
+		colorToHex(t.Primary),     // Selected line bg: purple (Charple) - matches TextSelected
+		colorToHex(t.BgSubtle),    // Inactive selected: subtle gray (Charcoal)
+		colorToHex(t.Success),     // Cherry-picked fg: green (Guac) - positive action
+		colorToHex(t.BgSubtle),    // Cherry-picked bg: subtle (Charcoal)
+		colorToHex(t.Info),        // Marked base fg: blue (Malibu) - distinct from cherry
+		colorToHex(t.BgSubtle),    // Marked base bg: subtle (Charcoal)
+		colorToHex(t.Error),       // Unstaged changes: red (Sriracha)
 	)
 
 	f, err := os.CreateTemp("", "crush-lazygit-*.yml")

internal/tui/components/dialogs/termdialog/termdialog.go 🔗

@@ -221,6 +221,27 @@ func (d *Dialog) ID() dialogs.DialogID {
 	return d.id
 }
 
+// Cursor returns the cursor position adjusted for the dialog's screen position.
+// Returns nil if the terminal cursor is hidden or not available.
+func (d *Dialog) Cursor() *tea.Cursor {
+	x, y := d.term.CursorPosition()
+	if x < 0 || y < 0 {
+		return nil
+	}
+
+	t := styles.CurrentTheme()
+	row, col := d.Position()
+	cursor := tea.NewCursor(x, y)
+	// Adjust for dialog position: border (1) + header height
+	cursor.X += col + 1
+	cursor.Y += row + 1 + headerHeight
+	// Match the app's cursor style
+	cursor.Color = t.Secondary
+	cursor.Shape = tea.CursorBlock
+	cursor.Blink = true
+	return cursor
+}
+
 func (d *Dialog) Close() tea.Cmd {
 	_ = d.term.Close()