Skip to content

Commit 5f78656

Browse files
feat: add OnColor, OnA11yChar, OnA11yTab event accessors to Terminal
Expose three event emitters that already exist on InputHandler but were missing from the public Terminal API: OnColor (OSC 4/10/11/12 color palette events), OnA11yChar (accessibility character announcements), and OnA11yTab (accessibility tab movement announcements). Also adds WithScreenReaderMode option to enable accessibility mode. Fixes #30 Co-authored-by: Ona <no-reply@ona.com>
1 parent 8f0ae92 commit 5f78656

2 files changed

Lines changed: 109 additions & 0 deletions

File tree

terminal.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ func WithScrollback(n int) Option {
2323
return func(o *TerminalOptions) { o.Scrollback = n }
2424
}
2525

26+
// WithScreenReaderMode enables screen reader / accessibility mode.
27+
func WithScreenReaderMode(on bool) Option {
28+
return func(o *TerminalOptions) { o.ScreenReaderMode = on }
29+
}
30+
2631
// Terminal is a headless terminal emulator.
2732
type Terminal struct {
2833
optionsService *OptionsService
@@ -220,6 +225,21 @@ func (t *Terminal) OnRender(fn func(RowRange)) Disposable {
220225
return t.OnRenderEmitter.Event(fn)
221226
}
222227

228+
// OnColor subscribes to color palette query/set/restore events (OSC 4/10/11/12).
229+
func (t *Terminal) OnColor(fn func([]ColorEvent)) Disposable {
230+
return t.inputHandler.OnColorEmitter.Event(fn)
231+
}
232+
233+
// OnA11yChar subscribes to accessibility character announcements.
234+
func (t *Terminal) OnA11yChar(fn func(string)) Disposable {
235+
return t.inputHandler.OnA11yCharEmitter.Event(fn)
236+
}
237+
238+
// OnA11yTab subscribes to accessibility tab movement announcements.
239+
func (t *Terminal) OnA11yTab(fn func(int)) Disposable {
240+
return t.inputHandler.OnA11yTabEmitter.Event(fn)
241+
}
242+
223243
// RegisterApcHandler registers a handler for APC escape sequences.
224244
// id identifies the APC function by its final character (e.g., Final: 'G' for Kitty graphics).
225245
func (t *Terminal) RegisterApcHandler(id FunctionIdentifier, handler func(data string) bool) Disposable {

terminal_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,4 +1090,93 @@ func TestTerminalScrollLinesFiresEvent(t *testing.T) {
10901090
}
10911091
}
10921092

1093+
func TestTerminalOnColor(t *testing.T) {
1094+
t.Parallel()
1095+
term := newTestTerminal(80, 24)
1096+
var got []ColorEvent
1097+
term.OnColor(func(events []ColorEvent) { got = events })
1098+
1099+
// OSC 4 ; 1 ; #ff0000 BEL — set indexed color 1 to red.
1100+
term.WriteString("\x1b]4;1;#ff0000\x07")
1101+
1102+
if len(got) == 0 {
1103+
t.Fatal("expected OnColor to fire, got no events")
1104+
}
1105+
if got[0].Type != ColorRequestSet {
1106+
t.Errorf("ColorEvent.Type = %v, want ColorRequestSet", got[0].Type)
1107+
}
1108+
if got[0].Index != 1 {
1109+
t.Errorf("ColorEvent.Index = %d, want 1", got[0].Index)
1110+
}
1111+
if got[0].Color == nil || *got[0].Color != (ColorRGB{0xff, 0x00, 0x00}) {
1112+
t.Errorf("ColorEvent.Color = %v, want &{255 0 0}", got[0].Color)
1113+
}
1114+
}
1115+
1116+
func TestTerminalOnColorDispose(t *testing.T) {
1117+
t.Parallel()
1118+
term := newTestTerminal(80, 24)
1119+
count := 0
1120+
d := term.OnColor(func([]ColorEvent) { count++ })
1121+
1122+
term.WriteString("\x1b]4;1;#ff0000\x07")
1123+
if count == 0 {
1124+
t.Fatal("expected OnColor to fire")
1125+
}
1126+
first := count
1127+
d.Dispose()
1128+
term.WriteString("\x1b]4;2;#00ff00\x07")
1129+
if count != first {
1130+
t.Errorf("OnColor fired after Dispose: count went from %d to %d", first, count)
1131+
}
1132+
}
1133+
1134+
func TestTerminalOnA11yTab(t *testing.T) {
1135+
t.Parallel()
1136+
term := New(WithCols(80), WithRows(24), WithScrollback(1000), WithScreenReaderMode(true))
1137+
var tabWidths []int
1138+
term.OnA11yTab(func(n int) { tabWidths = append(tabWidths, n) })
1139+
1140+
// Tab character triggers OnA11yTab when ScreenReaderMode is enabled.
1141+
term.WriteString("\t")
1142+
1143+
if len(tabWidths) == 0 {
1144+
t.Fatal("expected OnA11yTab to fire, got no events")
1145+
}
1146+
if tabWidths[0] != 8 {
1147+
t.Errorf("OnA11yTab width = %d, want 8 (default tab stop)", tabWidths[0])
1148+
}
1149+
}
1150+
1151+
func TestTerminalOnA11yTabDispose(t *testing.T) {
1152+
t.Parallel()
1153+
term := New(WithCols(80), WithRows(24), WithScrollback(1000), WithScreenReaderMode(true))
1154+
count := 0
1155+
d := term.OnA11yTab(func(int) { count++ })
1156+
1157+
term.WriteString("\t")
1158+
if count == 0 {
1159+
t.Fatal("expected OnA11yTab to fire")
1160+
}
1161+
first := count
1162+
d.Dispose()
1163+
term.WriteString("\t")
1164+
if count != first {
1165+
t.Errorf("OnA11yTab fired after Dispose: count went from %d to %d", first, count)
1166+
}
1167+
}
1168+
1169+
func TestTerminalOnA11yChar(t *testing.T) {
1170+
t.Parallel()
1171+
// OnA11yChar emitter exists and is subscribable, even if not yet fired
1172+
// by the current implementation. Verify the accessor returns a valid Disposable.
1173+
term := newTestTerminal(80, 24)
1174+
count := 0
1175+
d := term.OnA11yChar(func(string) { count++ })
1176+
if d == nil {
1177+
t.Fatal("OnA11yChar returned nil Disposable")
1178+
}
1179+
d.Dispose()
1180+
}
1181+
10931182

0 commit comments

Comments
 (0)