perf(vt): parse CSI and SGR params without split
- commit
- 56d89b09f6a84d3116b7aa733a30c62a713d9655
- author
- brice · · 2026-07-08 14:31 UTC
- committer
- brice · · 2026-07-08 20:59 UTC
- parent
- eff9002
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +161 -44
M pkg/vt/screen.go +104 -44
| @@ -81,8 +81,10 @@ type Screen struct { | |||
| 81 | 81 | defaultBG renderer.RGB | |
| 82 | 82 | defaultColorsKnown bool | |
| 83 | 83 | ||
| 84 | - | damage []renderer.Damage | |
| 85 | - | escapeBuf []byte | |
| 84 | + | damage []renderer.Damage | |
| 85 | + | escapeBuf []byte | |
| 86 | + | csiScratch []int | |
| 87 | + | sgrScratch []int | |
| 86 | 88 | ||
| 87 | 89 | scrollTop int | |
| 88 | 90 | scrollBottom int | |
| @@ -784,7 +786,7 @@ func consumeSTString(data []byte) (consumed int, partial bool) { | |||
| 784 | 786 | ||
| 785 | 787 | func (s *Screen) applyCSI(params string, cmd byte) { | |
| 786 | 788 | private := strings.HasPrefix(params, "?") | |
| 787 | - | parts := parseCSIInts(params) | |
| 789 | + | parts := s.parseCSIInts(params) | |
| 788 | 790 | switch cmd { | |
| 789 | 791 | case 'c': | |
| 790 | 792 | switch params { | |
| @@ -952,7 +954,7 @@ func (s *Screen) applySGR(params string) { | |||
| 952 | 954 | s.Style = renderer.DefaultStyle() | |
| 953 | 955 | return | |
| 954 | 956 | } | |
| 955 | - | parts := parseSGRInts(params) | |
| 957 | + | parts := s.parseSGRInts(params) | |
| 956 | 958 | for i := 0; i < len(parts); i++ { | |
| 957 | 959 | switch parts[i] { | |
| 958 | 960 | case 0: | |
| @@ -1309,66 +1311,124 @@ func (s *Screen) clampCursor() { | |||
| 1309 | 1311 | s.Col = clamp(s.Col, 0, s.Frame.Width-1) | |
| 1310 | 1312 | } | |
| 1311 | 1313 | ||
| 1314 | + | func (s *Screen) parseCSIInts(params string) []int { | |
| 1315 | + | s.csiScratch = parseCSIIntsInto(s.csiScratch[:0], params) | |
| 1316 | + | return s.csiScratch | |
| 1317 | + | } | |
| 1318 | + | ||
| 1312 | 1319 | func parseCSIInts(params string) []int { | |
| 1320 | + | return parseCSIIntsInto(nil, params) | |
| 1321 | + | } | |
| 1322 | + | ||
| 1323 | + | func parseCSIIntsInto(out []int, params string) []int { | |
| 1313 | 1324 | if params == "" { | |
| 1314 | - | return nil | |
| 1325 | + | return out | |
| 1315 | 1326 | } | |
| 1316 | 1327 | params = strings.TrimPrefix(params, "?") | |
| 1317 | 1328 | params = strings.TrimPrefix(params, ">") | |
| 1318 | - | parts := strings.Split(params, ";") | |
| 1319 | - | out := make([]int, 0, len(parts)) | |
| 1320 | - | for _, p := range parts { | |
| 1321 | - | out = append(out, parseCSIInt(p)) | |
| 1329 | + | start := 0 | |
| 1330 | + | for i := 0; i <= len(params); i++ { | |
| 1331 | + | if i == len(params) || params[i] == ';' { | |
| 1332 | + | out = append(out, parseCSIInt(params[start:i])) | |
| 1333 | + | start = i + 1 | |
| 1334 | + | } | |
| 1322 | 1335 | } | |
| 1323 | 1336 | return out | |
| 1324 | 1337 | } | |
| 1325 | 1338 | ||
| 1339 | + | func (s *Screen) parseSGRInts(params string) []int { | |
| 1340 | + | s.sgrScratch = parseSGRIntsInto(s.sgrScratch[:0], params) | |
| 1341 | + | return s.sgrScratch | |
| 1342 | + | } | |
| 1343 | + | ||
| 1326 | 1344 | func parseSGRInts(params string) []int { | |
| 1345 | + | return parseSGRIntsInto(nil, params) | |
| 1346 | + | } | |
| 1347 | + | ||
| 1348 | + | func parseSGRIntsInto(out []int, params string) []int { | |
| 1327 | 1349 | if params == "" { | |
| 1328 | - | return nil | |
| 1350 | + | return out | |
| 1329 | 1351 | } | |
| 1330 | - | groups := strings.Split(params, ";") | |
| 1331 | - | out := make([]int, 0, len(groups)) | |
| 1332 | - | for _, group := range groups { | |
| 1333 | - | if !strings.Contains(group, ":") { | |
| 1334 | - | out = append(out, parseCSIInt(group)) | |
| 1335 | - | continue | |
| 1352 | + | start := 0 | |
| 1353 | + | for i := 0; i <= len(params); i++ { | |
| 1354 | + | if i == len(params) || params[i] == ';' { | |
| 1355 | + | out = appendSGRGroup(out, params[start:i]) | |
| 1356 | + | start = i + 1 | |
| 1336 | 1357 | } | |
| 1337 | - | sub := strings.Split(group, ":") | |
| 1338 | - | code := parseCSIInt(sub[0]) | |
| 1339 | - | if code != 38 && code != 48 { | |
| 1340 | - | out = append(out, code) | |
| 1341 | - | continue | |
| 1358 | + | } | |
| 1359 | + | return out | |
| 1360 | + | } | |
| 1361 | + | ||
| 1362 | + | func appendSGRGroup(out []int, group string) []int { | |
| 1363 | + | colon := false | |
| 1364 | + | for i := 0; i < len(group); i++ { | |
| 1365 | + | if group[i] == ':' { | |
| 1366 | + | colon = true | |
| 1367 | + | break | |
| 1342 | 1368 | } | |
| 1343 | - | mode := 0 | |
| 1344 | - | if len(sub) > 1 { | |
| 1345 | - | mode = parseCSIInt(sub[1]) | |
| 1369 | + | } | |
| 1370 | + | if !colon { | |
| 1371 | + | return append(out, parseCSIInt(group)) | |
| 1372 | + | } | |
| 1373 | + | ||
| 1374 | + | parts := countSGRColonFields(group) | |
| 1375 | + | code := parseCSIInt(sgrColonField(group, 0)) | |
| 1376 | + | if code != 38 && code != 48 { | |
| 1377 | + | return append(out, code) | |
| 1378 | + | } | |
| 1379 | + | mode := 0 | |
| 1380 | + | if parts > 1 { | |
| 1381 | + | mode = parseCSIInt(sgrColonField(group, 1)) | |
| 1382 | + | } | |
| 1383 | + | switch mode { | |
| 1384 | + | case 5: | |
| 1385 | + | if parts < 3 { | |
| 1386 | + | return out | |
| 1346 | 1387 | } | |
| 1347 | - | switch mode { | |
| 1348 | - | case 5: | |
| 1349 | - | if len(sub) < 3 { | |
| 1350 | - | continue | |
| 1351 | - | } | |
| 1352 | - | out = append(out, code, mode, parseCSIInt(sub[2])) | |
| 1353 | - | case 2: | |
| 1354 | - | // code:mode::R:G:B and code:mode:cs:R:G:B both put RGB after | |
| 1355 | - | // the colorspace slot; code:mode:R:G:B omits that slot. | |
| 1356 | - | start := 2 | |
| 1357 | - | if len(sub) > 2 && (sub[2] == "" || len(sub) >= 6) { | |
| 1358 | - | start = 3 | |
| 1359 | - | } | |
| 1360 | - | if len(sub) < start+3 { | |
| 1361 | - | continue | |
| 1362 | - | } | |
| 1363 | - | out = append(out, code, mode) | |
| 1364 | - | for i := 0; i < 3; i++ { | |
| 1365 | - | out = append(out, parseCSIInt(sub[start+i])) | |
| 1366 | - | } | |
| 1388 | + | out = append(out, code, mode, parseCSIInt(sgrColonField(group, 2))) | |
| 1389 | + | case 2: | |
| 1390 | + | // code:mode::R:G:B and code:mode:cs:R:G:B both put RGB after | |
| 1391 | + | // the colorspace slot; code:mode:R:G:B omits that slot. | |
| 1392 | + | start := 2 | |
| 1393 | + | if parts > 2 && (sgrColonField(group, 2) == "" || parts >= 6) { | |
| 1394 | + | start = 3 | |
| 1395 | + | } | |
| 1396 | + | if parts < start+3 { | |
| 1397 | + | return out | |
| 1398 | + | } | |
| 1399 | + | out = append(out, code, mode) | |
| 1400 | + | for i := 0; i < 3; i++ { | |
| 1401 | + | out = append(out, parseCSIInt(sgrColonField(group, start+i))) | |
| 1367 | 1402 | } | |
| 1368 | 1403 | } | |
| 1369 | 1404 | return out | |
| 1370 | 1405 | } | |
| 1371 | 1406 | ||
| 1407 | + | func countSGRColonFields(group string) int { | |
| 1408 | + | count := 1 | |
| 1409 | + | for i := 0; i < len(group); i++ { | |
| 1410 | + | if group[i] == ':' { | |
| 1411 | + | count++ | |
| 1412 | + | } | |
| 1413 | + | } | |
| 1414 | + | return count | |
| 1415 | + | } | |
| 1416 | + | ||
| 1417 | + | func sgrColonField(group string, index int) string { | |
| 1418 | + | start := 0 | |
| 1419 | + | field := 0 | |
| 1420 | + | for i := 0; i <= len(group); i++ { | |
| 1421 | + | if i == len(group) || group[i] == ':' { | |
| 1422 | + | if field == index { | |
| 1423 | + | return group[start:i] | |
| 1424 | + | } | |
| 1425 | + | field++ | |
| 1426 | + | start = i + 1 | |
| 1427 | + | } | |
| 1428 | + | } | |
| 1429 | + | return "" | |
| 1430 | + | } | |
| 1431 | + | ||
| 1372 | 1432 | func parseCSIInt(param string) int { | |
| 1373 | 1433 | if param == "" { | |
| 1374 | 1434 | return 0 | |
| @@ -81,8 +81,10 @@ type Screen struct { | |||||
| 81 | defaultBG renderer.RGB | 81 | defaultBG renderer.RGB | ||
| 82 | defaultColorsKnown bool | 82 | defaultColorsKnown bool | ||
| 83 | 83 | ||||
| 84 | - | damage []renderer.Damage | 84 | + | damage []renderer.Damage |
| 85 | - | escapeBuf []byte | 85 | + | escapeBuf []byte |
| 86 | + | csiScratch []int | |||
| 87 | + | sgrScratch []int | |||
| 86 | 88 | ||||
| 87 | scrollTop int | 89 | scrollTop int | ||
| 88 | scrollBottom int | 90 | scrollBottom int | ||
| @@ -784,7 +786,7 @@ func consumeSTString(data []byte) (consumed int, partial bool) { | |||||
| 784 | 786 | ||||
| 785 | func (s *Screen) applyCSI(params string, cmd byte) { | 787 | func (s *Screen) applyCSI(params string, cmd byte) { | ||
| 786 | private := strings.HasPrefix(params, "?") | 788 | private := strings.HasPrefix(params, "?") | ||
| 787 | - | parts := parseCSIInts(params) | 789 | + | parts := s.parseCSIInts(params) |
| 788 | switch cmd { | 790 | switch cmd { | ||
| 789 | case 'c': | 791 | case 'c': | ||
| 790 | switch params { | 792 | switch params { | ||
| @@ -952,7 +954,7 @@ func (s *Screen) applySGR(params string) { | |||||
| 952 | s.Style = renderer.DefaultStyle() | 954 | s.Style = renderer.DefaultStyle() | ||
| 953 | return | 955 | return | ||
| 954 | } | 956 | } | ||
| 955 | - | parts := parseSGRInts(params) | 957 | + | parts := s.parseSGRInts(params) |
| 956 | for i := 0; i < len(parts); i++ { | 958 | for i := 0; i < len(parts); i++ { | ||
| 957 | switch parts[i] { | 959 | switch parts[i] { | ||
| 958 | case 0: | 960 | case 0: | ||
| @@ -1309,66 +1311,124 @@ func (s *Screen) clampCursor() { | |||||
| 1309 | s.Col = clamp(s.Col, 0, s.Frame.Width-1) | 1311 | s.Col = clamp(s.Col, 0, s.Frame.Width-1) | ||
| 1310 | } | 1312 | } | ||
| 1311 | 1313 | ||||
| 1314 | + | func (s *Screen) parseCSIInts(params string) []int { | |||
| 1315 | + | s.csiScratch = parseCSIIntsInto(s.csiScratch[:0], params) | |||
| 1316 | + | return s.csiScratch | |||
| 1317 | + | } | |||
| 1318 | + | ||||
| 1312 | func parseCSIInts(params string) []int { | 1319 | func parseCSIInts(params string) []int { | ||
| 1320 | + | return parseCSIIntsInto(nil, params) | |||
| 1321 | + | } | |||
| 1322 | + | ||||
| 1323 | + | func parseCSIIntsInto(out []int, params string) []int { | |||
| 1313 | if params == "" { | 1324 | if params == "" { | ||
| 1314 | - | return nil | 1325 | + | return out |
| 1315 | } | 1326 | } | ||
| 1316 | params = strings.TrimPrefix(params, "?") | 1327 | params = strings.TrimPrefix(params, "?") | ||
| 1317 | params = strings.TrimPrefix(params, ">") | 1328 | params = strings.TrimPrefix(params, ">") | ||
| 1318 | - | parts := strings.Split(params, ";") | 1329 | + | start := 0 |
| 1319 | - | out := make([]int, 0, len(parts)) | 1330 | + | for i := 0; i <= len(params); i++ { |
| 1320 | - | for _, p := range parts { | 1331 | + | if i == len(params) || params[i] == ';' { |
| 1321 | - | out = append(out, parseCSIInt(p)) | 1332 | + | out = append(out, parseCSIInt(params[start:i])) |
| 1333 | + | start = i + 1 | |||
| 1334 | + | } | |||
| 1322 | } | 1335 | } | ||
| 1323 | return out | 1336 | return out | ||
| 1324 | } | 1337 | } | ||
| 1325 | 1338 | ||||
| 1339 | + | func (s *Screen) parseSGRInts(params string) []int { | |||
| 1340 | + | s.sgrScratch = parseSGRIntsInto(s.sgrScratch[:0], params) | |||
| 1341 | + | return s.sgrScratch | |||
| 1342 | + | } | |||
| 1343 | + | ||||
| 1326 | func parseSGRInts(params string) []int { | 1344 | func parseSGRInts(params string) []int { | ||
| 1345 | + | return parseSGRIntsInto(nil, params) | |||
| 1346 | + | } | |||
| 1347 | + | ||||
| 1348 | + | func parseSGRIntsInto(out []int, params string) []int { | |||
| 1327 | if params == "" { | 1349 | if params == "" { | ||
| 1328 | - | return nil | 1350 | + | return out |
| 1329 | } | 1351 | } | ||
| 1330 | - | groups := strings.Split(params, ";") | 1352 | + | start := 0 |
| 1331 | - | out := make([]int, 0, len(groups)) | 1353 | + | for i := 0; i <= len(params); i++ { |
| 1332 | - | for _, group := range groups { | 1354 | + | if i == len(params) || params[i] == ';' { |
| 1333 | - | if !strings.Contains(group, ":") { | 1355 | + | out = appendSGRGroup(out, params[start:i]) |
| 1334 | - | out = append(out, parseCSIInt(group)) | 1356 | + | start = i + 1 |
| 1335 | - | continue | |||
| 1336 | } | 1357 | } | ||
| 1337 | - | sub := strings.Split(group, ":") | 1358 | + | } |
| 1338 | - | code := parseCSIInt(sub[0]) | 1359 | + | return out |
| 1339 | - | if code != 38 && code != 48 { | 1360 | + | } |
| 1340 | - | out = append(out, code) | 1361 | + | |
| 1341 | - | continue | 1362 | + | func appendSGRGroup(out []int, group string) []int { |
| 1363 | + | colon := false | |||
| 1364 | + | for i := 0; i < len(group); i++ { | |||
| 1365 | + | if group[i] == ':' { | |||
| 1366 | + | colon = true | |||
| 1367 | + | break | |||
| 1342 | } | 1368 | } | ||
| 1343 | - | mode := 0 | 1369 | + | } |
| 1344 | - | if len(sub) > 1 { | 1370 | + | if !colon { |
| 1345 | - | mode = parseCSIInt(sub[1]) | 1371 | + | return append(out, parseCSIInt(group)) |
| 1372 | + | } | |||
| 1373 | + | ||||
| 1374 | + | parts := countSGRColonFields(group) | |||
| 1375 | + | code := parseCSIInt(sgrColonField(group, 0)) | |||
| 1376 | + | if code != 38 && code != 48 { | |||
| 1377 | + | return append(out, code) | |||
| 1378 | + | } | |||
| 1379 | + | mode := 0 | |||
| 1380 | + | if parts > 1 { | |||
| 1381 | + | mode = parseCSIInt(sgrColonField(group, 1)) | |||
| 1382 | + | } | |||
| 1383 | + | switch mode { | |||
| 1384 | + | case 5: | |||
| 1385 | + | if parts < 3 { | |||
| 1386 | + | return out | |||
| 1346 | } | 1387 | } | ||
| 1347 | - | switch mode { | 1388 | + | out = append(out, code, mode, parseCSIInt(sgrColonField(group, 2))) |
| 1348 | - | case 5: | 1389 | + | case 2: |
| 1349 | - | if len(sub) < 3 { | 1390 | + | // code:mode::R:G:B and code:mode:cs:R:G:B both put RGB after |
| 1350 | - | continue | 1391 | + | // the colorspace slot; code:mode:R:G:B omits that slot. |
| 1351 | - | } | 1392 | + | start := 2 |
| 1352 | - | out = append(out, code, mode, parseCSIInt(sub[2])) | 1393 | + | if parts > 2 && (sgrColonField(group, 2) == "" || parts >= 6) { |
| 1353 | - | case 2: | 1394 | + | start = 3 |
| 1354 | - | // code:mode::R:G:B and code:mode:cs:R:G:B both put RGB after | 1395 | + | } |
| 1355 | - | // the colorspace slot; code:mode:R:G:B omits that slot. | 1396 | + | if parts < start+3 { |
| 1356 | - | start := 2 | 1397 | + | return out |
| 1357 | - | if len(sub) > 2 && (sub[2] == "" || len(sub) >= 6) { | 1398 | + | } |
| 1358 | - | start = 3 | 1399 | + | out = append(out, code, mode) |
| 1359 | - | } | 1400 | + | for i := 0; i < 3; i++ { |
| 1360 | - | if len(sub) < start+3 { | 1401 | + | out = append(out, parseCSIInt(sgrColonField(group, start+i))) |
| 1361 | - | continue | |||
| 1362 | - | } | |||
| 1363 | - | out = append(out, code, mode) | |||
| 1364 | - | for i := 0; i < 3; i++ { | |||
| 1365 | - | out = append(out, parseCSIInt(sub[start+i])) | |||
| 1366 | - | } | |||
| 1367 | } | 1402 | } | ||
| 1368 | } | 1403 | } | ||
| 1369 | return out | 1404 | return out | ||
| 1370 | } | 1405 | } | ||
| 1371 | 1406 | ||||
| 1407 | + | func countSGRColonFields(group string) int { | |||
| 1408 | + | count := 1 | |||
| 1409 | + | for i := 0; i < len(group); i++ { | |||
| 1410 | + | if group[i] == ':' { | |||
| 1411 | + | count++ | |||
| 1412 | + | } | |||
| 1413 | + | } | |||
| 1414 | + | return count | |||
| 1415 | + | } | |||
| 1416 | + | ||||
| 1417 | + | func sgrColonField(group string, index int) string { | |||
| 1418 | + | start := 0 | |||
| 1419 | + | field := 0 | |||
| 1420 | + | for i := 0; i <= len(group); i++ { | |||
| 1421 | + | if i == len(group) || group[i] == ':' { | |||
| 1422 | + | if field == index { | |||
| 1423 | + | return group[start:i] | |||
| 1424 | + | } | |||
| 1425 | + | field++ | |||
| 1426 | + | start = i + 1 | |||
| 1427 | + | } | |||
| 1428 | + | } | |||
| 1429 | + | return "" | |||
| 1430 | + | } | |||
| 1431 | + | ||||
| 1372 | func parseCSIInt(param string) int { | 1432 | func parseCSIInt(param string) int { | ||
| 1373 | if param == "" { | 1433 | if param == "" { | ||
| 1374 | return 0 | 1434 | return 0 | ||
M pkg/vt/screen_test.go +57 -0
| @@ -74,6 +74,63 @@ func lineText(s *Screen, y int) string { | |||
| 74 | 74 | return string(out) | |
| 75 | 75 | } | |
| 76 | 76 | ||
| 77 | + | func TestParseCSIInts(t *testing.T) { | |
| 78 | + | t.Parallel() | |
| 79 | + | ||
| 80 | + | tests := []struct { | |
| 81 | + | name string | |
| 82 | + | params string | |
| 83 | + | want []int | |
| 84 | + | }{ | |
| 85 | + | {name: "empty", params: "", want: nil}, | |
| 86 | + | {name: "single", params: "5", want: []int{5}}, | |
| 87 | + | {name: "multiple", params: "1;2;3", want: []int{1, 2, 3}}, | |
| 88 | + | {name: "leading empty token", params: ";5", want: []int{0, 5}}, | |
| 89 | + | {name: "double empty token", params: "5;;3", want: []int{5, 0, 3}}, | |
| 90 | + | {name: "trailing empty token", params: "5;", want: []int{5, 0}}, | |
| 91 | + | {name: "question prefix", params: "?25", want: []int{25}}, | |
| 92 | + | {name: "greater prefix", params: ">0;1", want: []int{0, 1}}, | |
| 93 | + | {name: "prefix order", params: "?>7", want: []int{7}}, | |
| 94 | + | {name: "junk token", params: "12x;4", want: []int{0, 4}}, | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | for _, tt := range tests { | |
| 98 | + | t.Run(tt.name, func(t *testing.T) { | |
| 99 | + | require.Equal(t, tt.want, parseCSIInts(tt.params)) | |
| 100 | + | }) | |
| 101 | + | } | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | func TestParseSGRInts(t *testing.T) { | |
| 105 | + | t.Parallel() | |
| 106 | + | ||
| 107 | + | tests := []struct { | |
| 108 | + | name string | |
| 109 | + | params string | |
| 110 | + | want []int | |
| 111 | + | }{ | |
| 112 | + | {name: "empty", params: "", want: nil}, | |
| 113 | + | {name: "single", params: "1", want: []int{1}}, | |
| 114 | + | {name: "multiple", params: "1;31", want: []int{1, 31}}, | |
| 115 | + | {name: "leading empty token", params: ";5", want: []int{0, 5}}, | |
| 116 | + | {name: "double empty token", params: "5;;3", want: []int{5, 0, 3}}, | |
| 117 | + | {name: "trailing empty token", params: "5;", want: []int{5, 0}}, | |
| 118 | + | {name: "indexed foreground colon", params: "38:5:196", want: []int{38, 5, 196}}, | |
| 119 | + | {name: "rgb foreground colon", params: "38:2:1:2:3", want: []int{38, 2, 1, 2, 3}}, | |
| 120 | + | {name: "indexed background colon", params: "48:5:10", want: []int{48, 5, 10}}, | |
| 121 | + | {name: "rgb foreground empty colorspace", params: "38:2::1:2:3", want: []int{38, 2, 1, 2, 3}}, | |
| 122 | + | {name: "rgb foreground explicit colorspace", params: "38:2:9:1:2:3", want: []int{38, 2, 1, 2, 3}}, | |
| 123 | + | {name: "non color colon group", params: "1:2:3", want: []int{1}}, | |
| 124 | + | {name: "junk token", params: "abc;31", want: []int{0, 31}}, | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | for _, tt := range tests { | |
| 128 | + | t.Run(tt.name, func(t *testing.T) { | |
| 129 | + | require.Equal(t, tt.want, parseSGRInts(tt.params)) | |
| 130 | + | }) | |
| 131 | + | } | |
| 132 | + | } | |
| 133 | + | ||
| 77 | 134 | func rowString(row []renderer.Cell) string { | |
| 78 | 135 | out := make([]rune, len(row)) | |
| 79 | 136 | for i, cell := range row { | |
| @@ -74,6 +74,63 @@ func lineText(s *Screen, y int) string { | |||||
| 74 | return string(out) | 74 | return string(out) | ||
| 75 | } | 75 | } | ||
| 76 | 76 | ||||
| 77 | + | func TestParseCSIInts(t *testing.T) { | |||
| 78 | + | t.Parallel() | |||
| 79 | + | ||||
| 80 | + | tests := []struct { | |||
| 81 | + | name string | |||
| 82 | + | params string | |||
| 83 | + | want []int | |||
| 84 | + | }{ | |||
| 85 | + | {name: "empty", params: "", want: nil}, | |||
| 86 | + | {name: "single", params: "5", want: []int{5}}, | |||
| 87 | + | {name: "multiple", params: "1;2;3", want: []int{1, 2, 3}}, | |||
| 88 | + | {name: "leading empty token", params: ";5", want: []int{0, 5}}, | |||
| 89 | + | {name: "double empty token", params: "5;;3", want: []int{5, 0, 3}}, | |||
| 90 | + | {name: "trailing empty token", params: "5;", want: []int{5, 0}}, | |||
| 91 | + | {name: "question prefix", params: "?25", want: []int{25}}, | |||
| 92 | + | {name: "greater prefix", params: ">0;1", want: []int{0, 1}}, | |||
| 93 | + | {name: "prefix order", params: "?>7", want: []int{7}}, | |||
| 94 | + | {name: "junk token", params: "12x;4", want: []int{0, 4}}, | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | for _, tt := range tests { | |||
| 98 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 99 | + | require.Equal(t, tt.want, parseCSIInts(tt.params)) | |||
| 100 | + | }) | |||
| 101 | + | } | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | func TestParseSGRInts(t *testing.T) { | |||
| 105 | + | t.Parallel() | |||
| 106 | + | ||||
| 107 | + | tests := []struct { | |||
| 108 | + | name string | |||
| 109 | + | params string | |||
| 110 | + | want []int | |||
| 111 | + | }{ | |||
| 112 | + | {name: "empty", params: "", want: nil}, | |||
| 113 | + | {name: "single", params: "1", want: []int{1}}, | |||
| 114 | + | {name: "multiple", params: "1;31", want: []int{1, 31}}, | |||
| 115 | + | {name: "leading empty token", params: ";5", want: []int{0, 5}}, | |||
| 116 | + | {name: "double empty token", params: "5;;3", want: []int{5, 0, 3}}, | |||
| 117 | + | {name: "trailing empty token", params: "5;", want: []int{5, 0}}, | |||
| 118 | + | {name: "indexed foreground colon", params: "38:5:196", want: []int{38, 5, 196}}, | |||
| 119 | + | {name: "rgb foreground colon", params: "38:2:1:2:3", want: []int{38, 2, 1, 2, 3}}, | |||
| 120 | + | {name: "indexed background colon", params: "48:5:10", want: []int{48, 5, 10}}, | |||
| 121 | + | {name: "rgb foreground empty colorspace", params: "38:2::1:2:3", want: []int{38, 2, 1, 2, 3}}, | |||
| 122 | + | {name: "rgb foreground explicit colorspace", params: "38:2:9:1:2:3", want: []int{38, 2, 1, 2, 3}}, | |||
| 123 | + | {name: "non color colon group", params: "1:2:3", want: []int{1}}, | |||
| 124 | + | {name: "junk token", params: "abc;31", want: []int{0, 31}}, | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | for _, tt := range tests { | |||
| 128 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 129 | + | require.Equal(t, tt.want, parseSGRInts(tt.params)) | |||
| 130 | + | }) | |||
| 131 | + | } | |||
| 132 | + | } | |||
| 133 | + | ||||
| 77 | func rowString(row []renderer.Cell) string { | 134 | func rowString(row []renderer.Cell) string { | ||
| 78 | out := make([]rune, len(row)) | 135 | out := make([]rune, len(row)) | ||
| 79 | for i, cell := range row { | 136 | for i, cell := range row { | ||