Skip to content

Commit 15b8673

Browse files
fix: clear all RGB bits when switching from RGB to P16/P256 colors
Use AttrRGBMask (0xFFFFFF) instead of AttrPColorMask (0xFF) when clearing color bits before setting a palette color. The narrower mask left stale bytes in the upper 16 bits of the 24-bit color field, causing incorrect raw Fg/Bg values after RGB-to-palette transitions. Port of upstream xterm.js commit 92339ac9. Fixes #22 Co-authored-by: Ona <no-reply@ona.com>
1 parent 5c6ea1c commit 15b8673

2 files changed

Lines changed: 95 additions & 7 deletions

File tree

inputhandler_sgr.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,22 @@ func (h *InputHandler) charAttributes(params *Params) bool {
2121
switch {
2222
case p >= 30 && p <= 37:
2323
// fg color 8
24-
attr.Fg &= ^(AttrCMMask | AttrPColorMask)
24+
attr.Fg &= ^(AttrCMMask | AttrRGBMask)
2525
attr.Fg |= AttrCMP16 | uint32(p-30)
2626

2727
case p >= 40 && p <= 47:
2828
// bg color 8
29-
attr.Bg &= ^(AttrCMMask | AttrPColorMask)
29+
attr.Bg &= ^(AttrCMMask | AttrRGBMask)
3030
attr.Bg |= AttrCMP16 | uint32(p-40)
3131

3232
case p >= 90 && p <= 97:
3333
// fg color 16 (bright)
34-
attr.Fg &= ^(AttrCMMask | AttrPColorMask)
34+
attr.Fg &= ^(AttrCMMask | AttrRGBMask)
3535
attr.Fg |= AttrCMP16 | uint32(p-90) | 8
3636

3737
case p >= 100 && p <= 107:
3838
// bg color 16 (bright)
39-
attr.Bg &= ^(AttrCMMask | AttrPColorMask)
39+
attr.Bg &= ^(AttrCMMask | AttrRGBMask)
4040
attr.Bg |= AttrCMP16 | uint32(p-100) | 8
4141

4242
case p == 0:
@@ -120,13 +120,13 @@ func (h *InputHandler) charAttributes(params *Params) bool {
120120
// reset fg
121121
attr.Fg &= ^(AttrCMMask | AttrRGBMask)
122122
def := DefaultAttrData()
123-
attr.Fg |= def.Fg & (AttrPColorMask | AttrRGBMask)
123+
attr.Fg |= def.Fg & AttrRGBMask
124124

125125
case p == 49:
126126
// reset bg
127127
attr.Bg &= ^(AttrCMMask | AttrRGBMask)
128128
def := DefaultAttrData()
129-
attr.Bg |= def.Bg & (AttrPColorMask | AttrRGBMask)
129+
attr.Bg |= def.Bg & AttrRGBMask
130130

131131
case p == 38 || p == 48 || p == 58:
132132
// extended color (fg/bg/underline)
@@ -171,7 +171,7 @@ func (h *InputHandler) updateAttrColor(color uint32, mode int32, c1, c2, c3 int3
171171
color &= ^AttrRGBMask
172172
color |= FromColorRGB(ColorRGB{uint8(c1), uint8(c2), uint8(c3)})
173173
case 5: // P256
174-
color &= ^(AttrCMMask | AttrPColorMask)
174+
color &= ^(AttrCMMask | AttrRGBMask)
175175
color |= AttrCMP256 | (uint32(c1) & 0xFF)
176176
}
177177
return color

inputhandler_sgr_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,94 @@ func TestSGR_SGR0_Preserves_URLId(t *testing.T) {
444444
}
445445
}
446446

447+
func TestSGR_RGBToP16_ClearsStaleRGBBits_Fg(t *testing.T) {
448+
t.Parallel()
449+
h := newTestHandler()
450+
// Set RGB fg #FF8040
451+
h.ParseString("\x1b[38;2;255;128;64m")
452+
// Switch to P16 red (index 1)
453+
h.ParseString("\x1b[31m")
454+
455+
// The full 24-bit color field must be clean — no stale RGB bytes
456+
colorBits := h.curAttrData.Fg & AttrRGBMask
457+
wantColor := uint32(1) // palette index 1 (red)
458+
if colorBits != wantColor {
459+
t.Errorf("stale RGB bits in Fg: got 0x%06x, want 0x%06x", colorBits, wantColor)
460+
}
461+
if cm := h.curAttrData.Fg & AttrCMMask; cm != AttrCMP16 {
462+
t.Errorf("wrong color mode: got 0x%x, want 0x%x (P16)", cm, AttrCMP16)
463+
}
464+
}
465+
466+
func TestSGR_RGBToP16_ClearsStaleRGBBits_Bg(t *testing.T) {
467+
t.Parallel()
468+
h := newTestHandler()
469+
// Set RGB bg #FF8040
470+
h.ParseString("\x1b[48;2;255;128;64m")
471+
// Switch to P16 green bg (index 2)
472+
h.ParseString("\x1b[42m")
473+
474+
colorBits := h.curAttrData.Bg & AttrRGBMask
475+
wantColor := uint32(2)
476+
if colorBits != wantColor {
477+
t.Errorf("stale RGB bits in Bg: got 0x%06x, want 0x%06x", colorBits, wantColor)
478+
}
479+
if cm := h.curAttrData.Bg & AttrCMMask; cm != AttrCMP16 {
480+
t.Errorf("wrong color mode: got 0x%x, want 0x%x (P16)", cm, AttrCMP16)
481+
}
482+
}
483+
484+
func TestSGR_RGBToP16Bright_ClearsStaleRGBBits(t *testing.T) {
485+
t.Parallel()
486+
h := newTestHandler()
487+
// Set RGB fg
488+
h.ParseString("\x1b[38;2;200;100;50m")
489+
// Switch to bright red fg (SGR 91 = index 1|8 = 9)
490+
h.ParseString("\x1b[91m")
491+
492+
colorBits := h.curAttrData.Fg & AttrRGBMask
493+
wantColor := uint32(1) | 8
494+
if colorBits != wantColor {
495+
t.Errorf("stale RGB bits in Fg after bright: got 0x%06x, want 0x%06x", colorBits, wantColor)
496+
}
497+
}
498+
499+
func TestSGR_RGBToP256_ClearsStaleRGBBits_Fg(t *testing.T) {
500+
t.Parallel()
501+
h := newTestHandler()
502+
// Set RGB fg #AABBCC
503+
h.ParseString("\x1b[38;2;170;187;204m")
504+
// Switch to P256 color 196
505+
h.ParseString("\x1b[38;5;196m")
506+
507+
colorBits := h.curAttrData.Fg & AttrRGBMask
508+
wantColor := uint32(196)
509+
if colorBits != wantColor {
510+
t.Errorf("stale RGB bits in Fg: got 0x%06x, want 0x%06x", colorBits, wantColor)
511+
}
512+
if cm := h.curAttrData.Fg & AttrCMMask; cm != AttrCMP256 {
513+
t.Errorf("wrong color mode: got 0x%x, want 0x%x (P256)", cm, AttrCMP256)
514+
}
515+
}
516+
517+
func TestSGR_RGBToP256_ClearsStaleRGBBits_Bg(t *testing.T) {
518+
t.Parallel()
519+
h := newTestHandler()
520+
// Set RGB bg
521+
h.ParseString("\x1b[48;2;170;187;204m")
522+
// Switch to P256 bg color 42
523+
h.ParseString("\x1b[48;5;42m")
524+
525+
colorBits := h.curAttrData.Bg & AttrRGBMask
526+
wantColor := uint32(42)
527+
if colorBits != wantColor {
528+
t.Errorf("stale RGB bits in Bg: got 0x%06x, want 0x%06x", colorBits, wantColor)
529+
}
530+
if cm := h.curAttrData.Bg & AttrCMMask; cm != AttrCMP256 {
531+
t.Errorf("wrong color mode: got 0x%x, want 0x%x (P256)", cm, AttrCMP256)
532+
}
533+
}
534+
447535
func TestSGR_MultipleResets(t *testing.T) {
448536
t.Parallel()
449537
h := newTestHandler()

0 commit comments

Comments
 (0)