Skip to content

Commit ac99a54

Browse files
authored
Mouse motion row and focus reporting fixes (#590)
* Report current focus state immediately when DECSET 1004 is enabled xterm reports the current focus state as soon as an application enables focus reporting; without it, applications that enable 1004 while the terminal is already focused assume they are unfocused until the first real focus change (e.g. Claude Code's Enter handling stays inert until the user clicks away and back). Terminal now tracks the host view's focus (setTerminalFocus updates it regardless of reporting mode) and DECSET 1004 replies with CSI I / CSI O for the tracked state right away. * Mac: report viewport rows in mouse motion events, drop stray print sendMotion was passed the buffer-absolute row (hit.grid.row includes yDisp) while press/release/drag paths already convert to screen rows; once any scrollback exists, hover-tracking applications receive rows far outside the viewport and their hit-testing never matches. Also removes a leftover debug print in the SGR-pixel mouse encoder that fires on every event.
1 parent ed395c7 commit ac99a54

3 files changed

Lines changed: 85 additions & 4 deletions

File tree

Sources/SwiftTerm/Mac/MacTerminalView.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2682,7 +2682,14 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations,
26822682

26832683
if terminal.mouseMode.sendMotionEvent() {
26842684
let flags = encodeMouseEvent(with: event, overwriteRelease: true)
2685-
terminal.sendMotion(buttonFlags: flags, x: hit.grid.col, y: hit.grid.row, pixelX: hit.pixels.col, pixelY: hit.pixels.row)
2685+
// Report the viewport row, not the buffer-absolute row: with
2686+
// scrollback present the absolute row is far outside the screen
2687+
// and applications tracking hover (e.g. TUIs with clickable rows)
2688+
// never match their hit targets. Press/release/drag already
2689+
// subtract yDisp via their own screenRow computations.
2690+
let displayBuffer = terminal.displayBuffer
2691+
let screenRow = max (0, min (displayBuffer.rows - 1, hit.grid.row - displayBuffer.yDisp))
2692+
terminal.sendMotion(buttonFlags: flags, x: hit.grid.col, y: screenRow, pixelX: hit.pixels.col, pixelY: hit.pixels.row)
26862693
}
26872694
}
26882695

Sources/SwiftTerm/Terminal.swift

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -569,13 +569,23 @@ open class Terminal {
569569
}
570570
}
571571

572+
/// Tracks the host view's focus so that enabling focus reporting (DECSET
573+
/// 1004) can immediately tell the application the current state, the way
574+
/// xterm does. Defaults to focused.
575+
var reportedFocusState: Bool = true
576+
572577
/// Invoke this command when the terminal receives and loses focus
573578
public func setTerminalFocus(_ focused: Bool) {
579+
reportedFocusState = focused
574580
if sendFocus {
575-
let data: [UInt8] = cc.CSI + [focused ? 0x49 : 0x4f]
576-
tdel?.send(source: self, data: data[0...])
581+
sendFocusReport()
577582
}
578583
}
584+
585+
func sendFocusReport() {
586+
let data: [UInt8] = cc.CSI + [reportedFocusState ? 0x49 : 0x4f]
587+
tdel?.send(source: self, data: data[0...])
588+
}
579589

580590
///
581591
/// Represents the mouse operation mode that the terminal is currently using and higher level
@@ -4406,6 +4416,10 @@ open class Terminal {
44064416
// focusin: ^[[I
44074417
// focusout: ^[[O
44084418
sendFocus = true
4419+
// Report the current state right away (xterm behavior), so
4420+
// the application does not assume it is unfocused until the
4421+
// first real focus change.
4422+
sendFocusReport()
44094423
case 1005:
44104424
// utf8 ext mode mouse
44114425
mouseProtocol = .utf8
@@ -5735,7 +5749,6 @@ open class Terminal {
57355749
let isRelease = (buttonFlags & 3) == 3 && (buttonFlags & 32) == 0
57365750
let bflags : Int = isRelease ? (buttonFlags & ~3) : buttonFlags
57375751
let m = isRelease ? "m" : "M"
5738-
print ("\(pixelX);\(pixelY)")
57395752
sendResponse(cc.CSI, "<\(bflags);\(pixelX);\(pixelY)\(m)")
57405753

57415754
case .urxvt:
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// FocusReportTests.swift
3+
//
4+
// DECSET 1004 focus reporting: enabling reports the current focus state
5+
// immediately (xterm behavior), then focus changes report as they happen.
6+
//
7+
import Foundation
8+
import Testing
9+
10+
@testable import SwiftTerm
11+
12+
final class FocusReportTests: TerminalDelegate {
13+
var sent: [UInt8] = []
14+
15+
func send(source: Terminal, data: ArraySlice<UInt8>) {
16+
sent.append(contentsOf: data)
17+
}
18+
19+
var sentString: String {
20+
String(decoding: sent, as: UTF8.self)
21+
}
22+
23+
func makeTerminal() -> Terminal {
24+
Terminal(delegate: self, options: TerminalOptions(cols: 80, rows: 25))
25+
}
26+
27+
@Test func enablingFocusReportingSendsCurrentState() {
28+
let terminal = makeTerminal()
29+
terminal.feed(text: "\u{1b}[?1004h")
30+
#expect(sentString == "\u{1b}[I", "enable while focused reports focus-in immediately")
31+
32+
sent.removeAll()
33+
terminal.setTerminalFocus(false)
34+
#expect(sentString == "\u{1b}[O")
35+
36+
sent.removeAll()
37+
terminal.setTerminalFocus(true)
38+
#expect(sentString == "\u{1b}[I")
39+
}
40+
41+
@Test func enablingWhileUnfocusedReportsFocusOut() {
42+
let terminal = makeTerminal()
43+
terminal.setTerminalFocus(false)
44+
sent.removeAll()
45+
terminal.feed(text: "\u{1b}[?1004h")
46+
#expect(sentString == "\u{1b}[O", "enable while unfocused reports focus-out immediately")
47+
}
48+
49+
@Test func focusChangesAreSilentWhenReportingDisabled() {
50+
let terminal = makeTerminal()
51+
terminal.setTerminalFocus(false)
52+
terminal.setTerminalFocus(true)
53+
#expect(sent.isEmpty)
54+
55+
terminal.feed(text: "\u{1b}[?1004h")
56+
sent.removeAll()
57+
terminal.feed(text: "\u{1b}[?1004l")
58+
terminal.setTerminalFocus(false)
59+
#expect(sent.isEmpty, "no reports after DECRST 1004")
60+
}
61+
}

0 commit comments

Comments
 (0)