feat(ui): seleksi baris pakai highlight (token Selection) gantikan panah — sesuai masukan
CI / test (push) Failing after 14s
CI / test (push) Failing after 14s
This commit is contained in:
1 parent
a28b83513d
commit
db891398fb
4 files changed
+138
-32
No files matched your search
+36
-7
@@ -149,6 +149,23 @@ func (u *tui) waitContains(substr string, d time.Duration) {
|
||||
u.t.Fatalf("layar tidak memuat %q dalam %v.\n--- snapshot layar ---\n%s", substr, d, u.screen())
|
||||
}
|
||||
|
||||
// waitHighlighted — menunggu baris yang memuat substr ber-latar (SGR bg),
|
||||
// yaitu penanda baris terpilih di daftar.
|
||||
func (u *tui) waitHighlighted(substr string, d time.Duration) {
|
||||
u.t.Helper()
|
||||
deadline := time.Now().Add(d)
|
||||
for time.Now().Before(deadline) {
|
||||
u.mu.Lock()
|
||||
ok := u.scr.Highlighted(substr)
|
||||
u.mu.Unlock()
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
u.t.Fatalf("baris %q tidak ter-highlight dalam %v.\n--- snapshot layar ---\n%s", substr, d, u.screen())
|
||||
}
|
||||
|
||||
func (u *tui) waitExit(d time.Duration) int {
|
||||
u.t.Helper()
|
||||
done := make(chan error, 1)
|
||||
@@ -347,8 +364,9 @@ func TestMouseAndNavigateKeys(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestClickSelectsContainer — lokal/CI-with-docker saja: klik baris sidebar
|
||||
// harus memindahkan penanda pilihan (▶) ke kontainer.
|
||||
// TestClickSelectsContainer — lokal/CI-with-docker saja: baris terpilih
|
||||
// ber-highlight (token Selection), klik berpindah seleksi, dan panel detail
|
||||
// mengikuti pilihan.
|
||||
func TestClickSelectsContainer(t *testing.T) {
|
||||
if _, err := os.Stat("/var/run/docker.sock"); err != nil {
|
||||
t.Skip("docker socket tidak ada")
|
||||
@@ -361,16 +379,27 @@ func TestClickSelectsContainer(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
items, err := cl.ListContainers(ctx)
|
||||
if err != nil || len(items) == 0 {
|
||||
t.Skipf("tidak ada kontainer (err=%v, n=%d)", err, len(items))
|
||||
if err != nil || len(items) < 2 {
|
||||
t.Skipf("butuh >= 2 kontainer (err=%v, n=%d)", err, len(items))
|
||||
}
|
||||
short := func(s string) string {
|
||||
if len(s) > 12 {
|
||||
return s[:12]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
u := startTUI(t)
|
||||
u.waitContains("beranda", 5*time.Second)
|
||||
u.send("1") // masuk view kontainer
|
||||
u.waitContains("Kontainer", 3*time.Second)
|
||||
u.send("\x1b[<0;10;4M\x1b[<0;10;4m") // klik baris item pertama sidebar
|
||||
u.waitContains("▶", 3*time.Second)
|
||||
u.waitContains(short(items[0].Name), 5*time.Second)
|
||||
u.waitHighlighted(short(items[0].Name), 3*time.Second) // seleksi awal (kursor 0)
|
||||
|
||||
// klik baris item ke-2: xterm y=4 (1-based) → app row 3 → indeks 1
|
||||
u.send("\x1b[<0;10;4M\x1b[<0;10;4m")
|
||||
u.waitHighlighted(short(items[1].Name), 3*time.Second)
|
||||
u.waitContains(items[1].ID, 3*time.Second) // panel detail ikut berpindah
|
||||
|
||||
u.send("q")
|
||||
if code := u.waitExit(5 * time.Second); code != 0 {
|
||||
t.Fatalf("exit code %d, mau 0", code)
|
||||
|
||||
+65
-15
@@ -13,11 +13,19 @@ import (
|
||||
// CUP (H/f), relative moves (A-D), G/d, erase line (K), erase display (J),
|
||||
// erase chars (X), save/restore cursor (ESC 7/8). SGR (m) & mode diam-diam
|
||||
// diabaikan.
|
||||
// cell — satu sel layar; bg menandai sel ditulis saat SGR latar aktif
|
||||
// (dipakai tes untuk memverifikasi baris ter-highlight).
|
||||
type cell struct {
|
||||
r rune
|
||||
bg bool
|
||||
}
|
||||
|
||||
type screen struct {
|
||||
rows, cols int
|
||||
grid [][]rune
|
||||
grid [][]cell
|
||||
curR, curC int
|
||||
savR, savC int
|
||||
bg bool // status SGR latar saat ini
|
||||
|
||||
st int // 0 normal · 1 esc · 2 esc-intern · 3 csi · 4 osc · 5 osc-esc
|
||||
seq []byte
|
||||
@@ -35,11 +43,11 @@ const (
|
||||
)
|
||||
|
||||
func newScreen(rows, cols int) *screen {
|
||||
g := make([][]rune, rows)
|
||||
g := make([][]cell, rows)
|
||||
for i := range g {
|
||||
g[i] = make([]rune, cols)
|
||||
g[i] = make([]cell, cols)
|
||||
for j := range g[i] {
|
||||
g[i][j] = ' '
|
||||
g[i][j] = cell{r: ' '}
|
||||
}
|
||||
}
|
||||
return &screen{rows: rows, cols: cols, grid: g}
|
||||
@@ -181,21 +189,21 @@ func (s *screen) applyCSI(final rune, params string) {
|
||||
switch at(nums, 0, 0) {
|
||||
case 0: // dari kursor sampai akhir layar
|
||||
for c := s.curC; c < s.cols; c++ {
|
||||
s.grid[s.curR][c] = ' '
|
||||
s.grid[s.curR][c] = cell{r: ' '}
|
||||
}
|
||||
for r := s.curR + 1; r < s.rows; r++ {
|
||||
for c := range s.grid[r] {
|
||||
s.grid[r][c] = ' '
|
||||
s.grid[r][c] = cell{r: ' '}
|
||||
}
|
||||
}
|
||||
case 1: // dari awal layar sampai kursor
|
||||
for r := 0; r < s.curR; r++ {
|
||||
for c := range s.grid[r] {
|
||||
s.grid[r][c] = ' '
|
||||
s.grid[r][c] = cell{r: ' '}
|
||||
}
|
||||
}
|
||||
for c := 0; c <= s.curC && c < s.cols; c++ {
|
||||
s.grid[s.curR][c] = ' '
|
||||
s.grid[s.curR][c] = cell{r: ' '}
|
||||
}
|
||||
default: // 2 (layar penuh) & 3 (scrollback) → bersihkan
|
||||
s.clearAll()
|
||||
@@ -205,18 +213,32 @@ func (s *screen) applyCSI(final rune, params string) {
|
||||
case 'X': // erase n karakter (kursor tidak pindah)
|
||||
n := at(nums, 0, 1)
|
||||
for i := 0; i < n && s.curC+i < s.cols; i++ {
|
||||
s.grid[s.curR][s.curC+i] = ' '
|
||||
s.grid[s.curR][s.curC+i] = cell{r: ' '}
|
||||
}
|
||||
case 'h':
|
||||
if priv == '?' && at(nums, 0, 0) == 1049 { // masuk alt-screen
|
||||
s.clearAll()
|
||||
}
|
||||
case 'm':
|
||||
// Lacak SGR latar: 48 (set bg) · 0/49 (reset). Dipakai sel & Highlighted().
|
||||
if len(nums) == 0 {
|
||||
s.bg = false
|
||||
return
|
||||
}
|
||||
for _, n := range nums {
|
||||
switch n {
|
||||
case 0, 49:
|
||||
s.bg = false
|
||||
case 48:
|
||||
s.bg = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *screen) put(r rune) {
|
||||
s.clampCursor()
|
||||
s.grid[s.curR][s.curC] = r
|
||||
s.grid[s.curR][s.curC] = cell{r: r, bg: s.bg}
|
||||
s.curC++
|
||||
}
|
||||
|
||||
@@ -238,14 +260,14 @@ func (s *screen) eraseLine(mode int) {
|
||||
switch mode {
|
||||
case 0: // dari kursor ke akhir
|
||||
if c >= s.curC {
|
||||
s.grid[s.curR][c] = ' '
|
||||
s.grid[s.curR][c] = cell{r: ' '}
|
||||
}
|
||||
case 1: // dari awal ke kursor
|
||||
if c <= s.curC {
|
||||
s.grid[s.curR][c] = ' '
|
||||
s.grid[s.curR][c] = cell{r: ' '}
|
||||
}
|
||||
case 2: // seluruh baris
|
||||
s.grid[s.curR][c] = ' '
|
||||
s.grid[s.curR][c] = cell{r: ' '}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,7 +275,7 @@ func (s *screen) eraseLine(mode int) {
|
||||
func (s *screen) clearAll() {
|
||||
for i := range s.grid {
|
||||
for j := range s.grid[i] {
|
||||
s.grid[i][j] = ' '
|
||||
s.grid[i][j] = cell{r: ' '}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,11 +284,39 @@ func (s *screen) clearAll() {
|
||||
func (s *screen) String() string {
|
||||
lines := make([]string, s.rows)
|
||||
for i, row := range s.grid {
|
||||
lines[i] = strings.TrimRight(string(row), " ")
|
||||
lines[i] = strings.TrimRight(rowString(row), " ")
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// Highlighted — true bila ada baris yang memuat substr dan posisi awal
|
||||
// substr itu berlatar (SGR bg aktif saat sel ditulis).
|
||||
func (s *screen) Highlighted(substr string) bool {
|
||||
if substr == "" {
|
||||
return false
|
||||
}
|
||||
for _, row := range s.grid {
|
||||
text := rowString(row)
|
||||
i := strings.Index(text, substr)
|
||||
if i < 0 {
|
||||
continue
|
||||
}
|
||||
pos := len([]rune(text[:i]))
|
||||
if pos < len(row) && row[pos].bg {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func rowString(row []cell) string {
|
||||
var b strings.Builder
|
||||
for _, c := range row {
|
||||
b.WriteRune(c.r)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func parseNums(s string) []int {
|
||||
if s == "" {
|
||||
return nil
|
||||
|
||||
Reference in new issue
Block a user