Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@gdamore
Copy link
Owner

@gdamore gdamore commented Dec 18, 2025

This is just very preliminary for now, but it lets us detect color and OSC 52 even if the value of $TERM is vt100 or similar.

I will add support for other negotiations so we can pick up the underlying terminal name from extended attributes to configure things like OSC 9 on iTerm2, and we can avoid enabling signal handlers when the terminal supports inline size notifications.

Summary by CodeRabbit

  • New Features

    • Detects and reports richer terminal primary capabilities (including clipboard and extended attributes) during startup.
    • Handles alternate query sequences to surface primary device attributes and finalize initialization.
  • Bug Fixes

    • More reliable startup sequencing with a timed initialization window and deferred color setup.
    • Non-blocking resize handling and improved wiring for terminal resize and clipboard-related flows.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 18, 2025

Walkthrough

Parses CSI primary device attribute (DA) sequences prefixed with '?' into an internal unexported eventPrimaryAttributes, posts that event; tScreen routes those events to a short init queue, processes them within ~1s to set color/clipboard flags, and completes initialization while wiring resize and input goroutines.

Changes

Cohort / File(s) Summary
CSI QM & Primary DA parsing
input.go
Add hasQM handling in CSI parsing; when a CSI starts with ? route parsed params to new handlePrimaryDA() which constructs an unexported eventPrimaryAttributes (Class + capability booleans + EventTime) and posts it instead of normal CSI handling.
tScreen init & event routing
tscreen.go
Add constants (requestPrimaryDA, setClipboard) and new state (noColor, initQ, initted); add filterEvents() to forward eventPrimaryAttributes to initQ, add processInitQ() to process a ~1s initialization window and set noColor/setClipboard/initted; change engage() to start input/main goroutines, issue requestPrimaryDA when uninitialized, wire NotifyResize, and reuse setClipboard in prepareExtendedOSC().

Sequence Diagram(s)

sequenceDiagram
    participant Term as Terminal (TTY)
    participant Parser as CSI Parser (input.go)
    participant EventBus as Event Router / postEvent
    participant Screen as tScreen (main)
    participant Init as initQ / processInitQ

    Term->>Parser: emit CSI sequence starting with "?"
    Parser->>Parser: detect hasQM, parse params -> build eventPrimaryAttributes
    Parser->>EventBus: post eventPrimaryAttributes
    EventBus->>Screen: filterEvents() routes eventPrimaryAttributes -> initQ
    Screen->>Init: enqueue into initQ
    Init->>Init: processInitQ() (1s window) evaluate attributes -> set noColor/setClipboard, mark initted
    Init->>Screen: signal initialization complete
    Note right of Screen: other events continue to main eventQ normally
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Review focus:
    • input.go — correctness of CSI parsing, hasQM detection, handlePrimaryDA() logic and event construction.
    • tscreen.go — concurrency around initQ vs eventQ, processInitQ() timing, and goroutine lifecycle in engage().
    • Interaction/races between filterEvents() and existing event consumers, and correctness of NotifyResize wiring.

Possibly related PRs

Poem

🐰 I nudge a question-mark and watch it bloom,
Tiny attributes hop out of the gloom.
One second's pause, a clipboard and hue,
The screen wakes up, refreshed and new.
Thump — I dance where bytes become view.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a negotiation step to detect and handle terminal attributes through primary DA capability detection.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch gdamore/negotiate-da

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7eb16b and cb2c29d.

📒 Files selected for processing (2)
  • input.go (4 hunks)
  • tscreen.go (10 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-18T02:50:37.669Z
Learnt from: gdamore
Repo: gdamore/tcell PR: 908
File: stdin_unix.go:128-155
Timestamp: 2025-12-18T02:50:37.669Z
Learning: In the tcell Tty interface implementations (stdin_unix.go, tty_unix.go, tty_win.go, tty_plan9.go), the caller is responsible for providing synchronization/mutual exclusion guarantees for method calls like NotifyResize. The Tty implementations themselves do not need internal locking for these methods, as documented in tty.go. The tscreen.go layer handles the synchronization.

Applied to files:

  • tscreen.go
🧬 Code graph analysis (2)
input.go (2)
color.go (1)
  • Color (32-32)
event.go (1)
  • EventTime (30-32)
tscreen.go (2)
event.go (1)
  • Event (23-26)
color.go (2)
  • Color (32-32)
  • ColorValid (42-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (windows-latest)
🔇 Additional comments (8)
input.go (2)

841-882: Clean integration of QM-prefixed CSI handling.

The hasQM flag follows the same pattern as the existing hasLT handling. The routing to handlePrimaryDA for mode 'c' is correct for processing primary device attribute responses.


996-1010: Struct design is appropriate for internal event routing.

Embedding EventTime correctly provides the When() method to satisfy the Event interface. The capability flags align with standard DEC VT DA response codes.

tscreen.go (6)

103-104: Constants are correct.

The requestPrimaryDA escape sequence (\x1b[c) is the standard CSI DA request, and setClipboard follows the OSC 52 specification with proper ST terminator.


284-312: Blocking initialization with lock held is intentional but may delay concurrent operations.

The function blocks up to 1 second while holding the screen lock. This is acceptable during initialization since:

  • filterEvents goroutine doesn't acquire the lock
  • mainLoop resize handling uses a goroutine (line 990) to avoid blocking
  • The eventQ buffer (128) is unlikely to fill during this window

However, any external code calling methods that require t.Lock() during this 1-second window will block. Consider adding a brief comment in engage() noting this design constraint for future maintainers.


314-336: Goroutine lifecycle is now properly managed.

The addition of case <-t.quit: addresses the previously flagged goroutine leak concern. The non-blocking send to initQ (lines 326-329) prevents blocking if the init queue fills, which is appropriate since init events are only relevant during the brief initialization window.


358-358: Good consolidation using the constant.

Using the setClipboard constant here maintains consistency with other escape sequence definitions.


990-998: Goroutine for resize handling prevents blocking mainLoop.

This change ensures the main event loop remains responsive while the lock is held (e.g., during processInitQ). The lock serializes concurrent resize operations, so rapid resize events will queue up but execute in order. Since resize events are typically infrequent, this is acceptable.


1112-1154: Initialization sequence is correctly ordered.

The flow is logical:

  1. Start input/main loops (lines 1116-1117) so they're ready to process responses
  2. Send DA request if needed (line 1120)
  3. Wait for response via processInitQ (line 1122)
  4. Complete terminal setup
  5. Wire resize notifications last (line 1153)

Moving NotifyResize to the end is acceptable since WindowSize() (line 1125) captures the current dimensions at initialization time.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tscreen.go (1)

1104-1145: Race condition: wg.Add(2) called after goroutines start.

t.wg.Add(2) at line 1145 is called after the goroutines are started at lines 1107-1108. Both inputLoop and mainLoop have defer t.wg.Done() at the start. If either goroutine exits before wg.Add(2) is called (e.g., immediate read error), it will call Done() on a zero-valued counter, causing a panic: sync: negative WaitGroup counter.

🔎 Apply this diff to fix the race condition:
 	stopQ := make(chan struct{})
 	t.stopQ = stopQ
+	t.wg.Add(2)
 	go t.inputLoop(stopQ)
 	go t.mainLoop(stopQ)

 	if !t.initted {
 		t.Print(requestPrimaryDA)
 	}
 	t.processInitQ()
 
 	t.running = true
 	// ... rest of engage() ...
 
 	t.tty.NotifyResize(t.resizeQ)
-	t.wg.Add(2)
 	return nil
🧹 Nitpick comments (1)
tscreen.go (1)

314-330: Silent event dropping if initQ is full.

Lines 320-323 use a non-blocking send to initQ. If the channel is full, the eventPrimaryAttributes event is silently dropped. While this is unlikely to occur in practice (initQ has capacity 32), consider logging when this happens for debugging purposes.

Additionally, the goroutine started here has no explicit shutdown mechanism. The channel inQ is never closed, so this goroutine will remain alive for the lifetime of the process. For a terminal application this is acceptable, but worth noting.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7796b4 and 2829017.

📒 Files selected for processing (2)
  • input.go (4 hunks)
  • tscreen.go (9 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-18T02:50:37.669Z
Learnt from: gdamore
Repo: gdamore/tcell PR: 908
File: stdin_unix.go:128-155
Timestamp: 2025-12-18T02:50:37.669Z
Learning: In the tcell Tty interface implementations (stdin_unix.go, tty_unix.go, tty_win.go, tty_plan9.go), the caller is responsible for providing synchronization/mutual exclusion guarantees for method calls like NotifyResize. The Tty implementations themselves do not need internal locking for these methods, as documented in tty.go. The tscreen.go layer handles the synchronization.

Applied to files:

  • tscreen.go
🧬 Code graph analysis (2)
input.go (2)
color.go (1)
  • Color (32-32)
event.go (1)
  • EventTime (30-32)
tscreen.go (2)
event.go (1)
  • Event (23-26)
color.go (2)
  • Color (32-32)
  • ColorValid (42-42)
🔇 Additional comments (6)
input.go (2)

795-826: LGTM!

The handlePrimaryDA function correctly parses the primary device attributes response. The early return for empty params is appropriate, and the capability flag mapping aligns with standard DA attribute codes.


841-882: LGTM!

The CSI handling changes correctly detect and route question-mark prefixed sequences. The pattern mirrors the existing hasLT handling for mouse events, and the early return prevents fallthrough to standard CSI processing.

tscreen.go (4)

103-104: LGTM!

The new constants correctly define the primary DA request sequence and OSC 52 clipboard format.


251-279: LGTM!

The initialization flow changes are well-structured. Setting up initQ and filterEvents before engage() ensures the event routing is ready when the input/main loops start. Moving the color palette initialization after engage() correctly accounts for potential ncolor updates from the DA response.


352-352: LGTM!

Using the setClipboard constant instead of a hardcoded string improves consistency and maintainability.


284-312: Potential blocking issue: Lock held during 1-second timeout.

The comment states this is called with the lock held, and indeed engage() calls this while holding t.Lock(). This function can block for up to 1 second waiting for the DA response, during which all other operations requiring the tScreen lock will be blocked.

While the data flow for receiving the DA response should work (inputLoop → keychan → mainLoop → event parsing), any concurrent access to the screen (e.g., resize events, Show/Sync calls from other goroutines) will be blocked during initialization.

Consider releasing the lock before blocking on the channel, or restructuring to avoid holding the lock during the wait. For example:

func (t *tScreen) processInitQ() {
    t.Lock()
    if t.initted {
        t.Unlock()
        return
    }
    t.Unlock()

    expire := time.After(time.Second)
    for {
        select {
        case <-expire:
            t.Lock()
            t.initted = true
            t.Unlock()
            return
        case ev := <-t.initQ:
            // ... process event ...
            t.Lock()
            // ... update state ...
            t.initted = true
            t.Unlock()
            return
        }
    }
}
⛔ Skipped due to learnings
Learnt from: gdamore
Repo: gdamore/tcell PR: 908
File: stdin_unix.go:128-155
Timestamp: 2025-12-18T02:50:37.669Z
Learning: In the tcell Tty interface implementations (stdin_unix.go, tty_unix.go, tty_win.go, tty_plan9.go), the caller is responsible for providing synchronization/mutual exclusion guarantees for method calls like NotifyResize. The Tty implementations themselves do not need internal locking for these methods, as documented in tty.go. The tscreen.go layer handles the synchronization.

@gdamore gdamore force-pushed the gdamore/negotiate-da branch from 2829017 to e7eb16b Compare December 18, 2025 07:09
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
input.go (1)

1004-1004: Typo: "SerboCroation" should be "SerboCroatian".

This also requires updating the corresponding assignment in handlePrimaryDA (line 811).

🧹 Nitpick comments (1)
tscreen.go (1)

284-312: Potential blocking with lock held in processInitQ.

The comment indicates this is "called with lock held", yet the function blocks for up to 1 second waiting on initQ. During this time, any other goroutine attempting to acquire t.Lock() (e.g., mainLoop on resize events) will be blocked.

While this may be acceptable during initial startup (before external callers interact with the screen), consider either:

  1. Releasing the lock before blocking on the channel, or
  2. Using a shorter timeout with retry logic, or
  3. Documenting this behavior explicitly
🔎 Alternative approach releasing lock during wait:
 func (t *tScreen) processInitQ() {
-	// NB: called with lock held
 	if t.initted {
 		return
 	}

 	expire := time.After(time.Second)
+	t.Unlock()
+	defer t.Lock()

 	for {
 		select {
 		case <-expire:
+			t.Lock()
 			t.initted = true
+			t.Unlock()
 			return
 		case ev := <-t.initQ:
 			switch ev := ev.(type) {
 			case *eventPrimaryAttributes:
+				t.Lock()
 				if ev.Color && t.ncolor == 0 && !t.noColor {
 					t.ncolor = 8
 				}
 				if ev.Clipboard && t.setClipboard == "" {
 					t.setClipboard = setClipboard
 				}
 				t.initted = true
+				t.Unlock()
 				return
 			}
 		}
 	}
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2829017 and e7eb16b.

📒 Files selected for processing (2)
  • input.go (4 hunks)
  • tscreen.go (9 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-18T02:50:37.669Z
Learnt from: gdamore
Repo: gdamore/tcell PR: 908
File: stdin_unix.go:128-155
Timestamp: 2025-12-18T02:50:37.669Z
Learning: In the tcell Tty interface implementations (stdin_unix.go, tty_unix.go, tty_win.go, tty_plan9.go), the caller is responsible for providing synchronization/mutual exclusion guarantees for method calls like NotifyResize. The Tty implementations themselves do not need internal locking for these methods, as documented in tty.go. The tscreen.go layer handles the synchronization.

Applied to files:

  • tscreen.go
🧬 Code graph analysis (2)
tscreen.go (2)
event.go (1)
  • Event (23-26)
color.go (2)
  • Color (32-32)
  • ColorValid (42-42)
input.go (1)
event.go (1)
  • EventTime (30-32)
🔇 Additional comments (7)
input.go (2)

795-826: LGTM on handlePrimaryDA parsing logic.

The function correctly handles the DA response format, extracting the class from the first parameter and parsing capability flags for VT200+ terminals (class >= 60). The switch-case mapping of DA values to boolean flags is appropriate.


841-882: LGTM on QM-prefixed CSI handling.

The addition of hasQM handling follows the existing pattern for hasLT, cleanly routing DA responses (?...c sequences) to handlePrimaryDA while preserving other CSI processing.

tscreen.go (5)

103-104: LGTM on new escape sequence constants.

The constants for requestPrimaryDA and setClipboard centralize the escape sequences appropriately.


152-166: LGTM on new state fields.

The noColor, initQ, and initted fields cleanly separate the initialization state from runtime state.


271-279: LGTM on deferred palette initialization.

Moving the color palette setup after engage() correctly allows the DA response to update t.ncolor before the palette is built. This enables color support even when $TERM indicates a limited terminal.


1104-1145: LGTM on engage() restructuring.

The revised flow correctly:

  1. Starts input/main loops first to receive DA responses
  2. Sends the DA request only if not already initialized
  3. Waits for initialization to complete
  4. Proceeds with the rest of terminal setup

The sequence ensures the DA response can be processed before capabilities are used.


352-352: LGTM on using setClipboard constant.

This correctly uses the new constant instead of a duplicate hardcoded string.

This is just very preliminary for now, but it lets us detect color
and OSC 52 even if the value of $TERM is vt100 or similar.
@gdamore gdamore force-pushed the gdamore/negotiate-da branch from e7eb16b to cb2c29d Compare December 18, 2025 07:21
@gdamore gdamore merged commit 1905981 into main Dec 18, 2025
13 checks passed
@gdamore gdamore deleted the gdamore/negotiate-da branch December 18, 2025 07:30
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.

1 participant