diff --git a/internal/cmd/course/course.go b/internal/cmd/course/course.go index 511fd9d..8701347 100644 --- a/internal/cmd/course/course.go +++ b/internal/cmd/course/course.go @@ -85,6 +85,9 @@ func runCourseList(cmd *cobra.Command, opts courseListOpts) error { } return tui.TableResult{Rows: list.Rows, Total: list.Total, Page: list.Page}, nil }, + OnSelect: func(row map[string]any) error { + return runCourseView(cmd, fmt.Sprint(row["jwId"])) + }, EmptyMessage: "No courses found. Try a broader search.", }) } @@ -126,7 +129,7 @@ func courseListColumns() []output.Column { {Header: "Code", Key: "code"}, {Header: "Name", Key: "namePrimary"}, {Header: "Level", Key: "educationLevel.name"}, - {Header: "ID", Key: "id"}, + {Header: "JW ID", Key: "jwId"}, } } @@ -161,46 +164,50 @@ func newCmdView() *cobra.Command { Short: "View course details", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - c, err := api.NewTypedClient(cmdutil.ServerFromCmd(cmd), false) - if err != nil { - return err - } - jwID, err := cmdutil.Int64PtrIfSet(args[0]) - if err != nil { - return err - } - data, err := api.ParseResponseRaw(c.GetCourse(api.Ctx(), *jwID)) - if err != nil { - return err - } - if output.IsJSON() { - return output.JSON(data) - } - m := cmdutil.AsMap(data) - output.KVWithTitle([]output.KVPair{ - {Key: "ID", Value: output.Resolve(m, "id")}, - {Key: "Code", Value: output.Resolve(m, "code")}, - {Key: "Name", Value: output.Resolve(m, "namePrimary")}, - {Key: "Name (EN)", Value: output.Resolve(m, "nameSecondary")}, - {Key: "Level", Value: output.Resolve(m, "educationLevel.name")}, - {Key: "Category", Value: output.Resolve(m, "category.name")}, - {Key: "Class type", Value: output.Resolve(m, "classType.name")}, - {Key: "Gradation", Value: output.Resolve(m, "gradation.name")}, - {Key: "Course type", Value: output.Resolve(m, "type.name")}, - }, "Course") - - if sections, ok := m["sections"].([]any); ok && len(sections) > 0 { - fmt.Println() - output.Bold(" Sections") - rows := cmdutil.RowsFromAny(sections) - output.Table(rows, []output.Column{ - {Header: "ID", Key: "id"}, - {Header: "Code", Key: "code"}, - {Header: "Semester", Key: "semester.name"}, - {Header: "Campus", Key: "campus.name"}, - }) - } - return nil + return runCourseView(cmd, args[0]) }, } } + +func runCourseView(cmd *cobra.Command, id string) error { + c, err := api.NewTypedClient(cmdutil.ServerFromCmd(cmd), false) + if err != nil { + return err + } + jwID, err := cmdutil.Int64PtrIfSet(id) + if err != nil { + return err + } + data, err := api.ParseResponseRaw(c.GetCourse(api.Ctx(), *jwID)) + if err != nil { + return err + } + if output.IsJSON() { + return output.JSON(data) + } + m := cmdutil.AsMap(data) + output.KVWithTitle([]output.KVPair{ + {Key: "ID", Value: output.Resolve(m, "id")}, + {Key: "Code", Value: output.Resolve(m, "code")}, + {Key: "Name", Value: output.Resolve(m, "namePrimary")}, + {Key: "Name (EN)", Value: output.Resolve(m, "nameSecondary")}, + {Key: "Level", Value: output.Resolve(m, "educationLevel.name")}, + {Key: "Category", Value: output.Resolve(m, "category.name")}, + {Key: "Class type", Value: output.Resolve(m, "classType.name")}, + {Key: "Gradation", Value: output.Resolve(m, "gradation.name")}, + {Key: "Course type", Value: output.Resolve(m, "type.name")}, + }, "Course") + + if sections, ok := m["sections"].([]any); ok && len(sections) > 0 { + fmt.Println() + output.Bold(" Sections") + rows := cmdutil.RowsFromAny(sections) + output.Table(rows, []output.Column{ + {Header: "ID", Key: "id"}, + {Header: "Code", Key: "code"}, + {Header: "Semester", Key: "semester.name"}, + {Header: "Campus", Key: "campus.name"}, + }) + } + return nil +} diff --git a/internal/cmd/course/course_test.go b/internal/cmd/course/course_test.go new file mode 100644 index 0000000..f16b0ef --- /dev/null +++ b/internal/cmd/course/course_test.go @@ -0,0 +1,11 @@ +package course + +import "testing" + +func TestCourseListUsesViewIdentifier(t *testing.T) { + columns := courseListColumns() + last := columns[len(columns)-1] + if last.Header != "JW ID" || last.Key != "jwId" { + t.Fatalf("last column = %#v, want JW ID backed by jwId", last) + } +} diff --git a/internal/cmd/section/section.go b/internal/cmd/section/section.go index 6ae2f8e..5de6809 100644 --- a/internal/cmd/section/section.go +++ b/internal/cmd/section/section.go @@ -107,6 +107,9 @@ func runSectionList(cmd *cobra.Command, opts sectionListOpts) error { } return tui.TableResult{Rows: list.Rows, Total: list.Total, Page: list.Page}, nil }, + OnSelect: func(row map[string]any) error { + return runSectionView(cmd, fmt.Sprint(row["jwId"])) + }, EmptyMessage: "No sections found. Try a broader search.", }) } @@ -156,7 +159,7 @@ func sectionListColumns() []output.Column { {Header: "Course", Key: "course.namePrimary"}, {Header: "Semester", Key: "semester.name"}, {Header: "Campus", Key: "campus.name"}, - {Header: "ID", Key: "id"}, + {Header: "JW ID", Key: "jwId"}, } } @@ -194,60 +197,64 @@ func newCmdView() *cobra.Command { Short: "View section details", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - c, err := api.NewTypedClient(cmdutil.ServerFromCmd(cmd), false) - if err != nil { - return err - } - jwID, err := cmdutil.Int64PtrIfSet(args[0]) - if err != nil { - return err - } - data, err := api.ParseResponseRaw(c.GetSection(api.Ctx(), *jwID)) - if err != nil { - return err - } - if output.IsJSON() { - return output.JSON(data) - } - m := cmdutil.AsMap(data) - output.KVWithTitle([]output.KVPair{ - {Key: "ID", Value: output.Resolve(m, "id")}, - {Key: "Code", Value: output.Resolve(m, "code")}, - {Key: "Course", Value: output.Resolve(m, "course.namePrimary")}, - {Key: "Semester", Value: output.Resolve(m, "semester.name")}, - {Key: "Campus", Value: output.Resolve(m, "campus.name")}, - }, "Section") + return runSectionView(cmd, args[0]) + }, + } +} - if teachers, ok := m["teachers"].([]any); ok && len(teachers) > 0 { - fmt.Println() - output.Bold(" Teachers") - rows := cmdutil.RowsFromAny(teachers) - output.Table(rows, []output.Column{ - {Header: "ID", Key: "id"}, - {Header: "Name", Key: "namePrimary"}, - {Header: "Name (EN)", Key: "nameSecondary"}, - {Header: "Department", Key: "department.name"}, - }) - } +func runSectionView(cmd *cobra.Command, id string) error { + c, err := api.NewTypedClient(cmdutil.ServerFromCmd(cmd), false) + if err != nil { + return err + } + jwID, err := cmdutil.Int64PtrIfSet(id) + if err != nil { + return err + } + data, err := api.ParseResponseRaw(c.GetSection(api.Ctx(), *jwID)) + if err != nil { + return err + } + if output.IsJSON() { + return output.JSON(data) + } + m := cmdutil.AsMap(data) + output.KVWithTitle([]output.KVPair{ + {Key: "ID", Value: output.Resolve(m, "id")}, + {Key: "Code", Value: output.Resolve(m, "code")}, + {Key: "Course", Value: output.Resolve(m, "course.namePrimary")}, + {Key: "Semester", Value: output.Resolve(m, "semester.name")}, + {Key: "Campus", Value: output.Resolve(m, "campus.name")}, + }, "Section") - if schedules, ok := m["schedules"].([]any); ok && len(schedules) > 0 { - fmt.Println() - output.Bold(" Schedules") - rows := cmdutil.RowsFromAny(schedules) - for _, row := range rows { - normalizeScheduleRow(row) - } - output.Table(rows, []output.Column{ - {Header: "ID", Key: "id"}, - {Header: "Day", Key: "weekday"}, - {Header: "Start", Key: "startTime"}, - {Header: "End", Key: "endTime"}, - {Header: "Place", Key: "customPlace"}, - }) - } - return nil - }, + if teachers, ok := m["teachers"].([]any); ok && len(teachers) > 0 { + fmt.Println() + output.Bold(" Teachers") + rows := cmdutil.RowsFromAny(teachers) + output.Table(rows, []output.Column{ + {Header: "ID", Key: "id"}, + {Header: "Name", Key: "namePrimary"}, + {Header: "Name (EN)", Key: "nameSecondary"}, + {Header: "Department", Key: "department.name"}, + }) + } + + if schedules, ok := m["schedules"].([]any); ok && len(schedules) > 0 { + fmt.Println() + output.Bold(" Schedules") + rows := cmdutil.RowsFromAny(schedules) + for _, row := range rows { + normalizeScheduleRow(row) + } + output.Table(rows, []output.Column{ + {Header: "ID", Key: "id"}, + {Header: "Day", Key: "weekday"}, + {Header: "Start", Key: "startTime"}, + {Header: "End", Key: "endTime"}, + {Header: "Place", Key: "customPlace"}, + }) } + return nil } func newCmdSchedules() *cobra.Command { diff --git a/internal/cmd/section/section_test.go b/internal/cmd/section/section_test.go index 0624475..83fd6dc 100644 --- a/internal/cmd/section/section_test.go +++ b/internal/cmd/section/section_test.go @@ -119,3 +119,11 @@ func TestNormalizeScheduleRow_FullRow(t *testing.T) { t.Errorf("endTime: got %v, want 10:40", got) } } + +func TestSectionListUsesViewIdentifier(t *testing.T) { + columns := sectionListColumns() + last := columns[len(columns)-1] + if last.Header != "JW ID" || last.Key != "jwId" { + t.Fatalf("last column = %#v, want JW ID backed by jwId", last) + } +} diff --git a/internal/cmd/teacher/teacher.go b/internal/cmd/teacher/teacher.go index 53b2944..b9ff33d 100644 --- a/internal/cmd/teacher/teacher.go +++ b/internal/cmd/teacher/teacher.go @@ -84,6 +84,9 @@ func runTeacherList(cmd *cobra.Command, opts teacherListOpts) error { } return tui.TableResult{Rows: list.Rows, Total: list.Total, Page: list.Page}, nil }, + OnSelect: func(row map[string]any) error { + return runTeacherView(cmd, fmt.Sprint(row["id"])) + }, EmptyMessage: "No teachers found. Try a broader search.", }) } @@ -152,43 +155,47 @@ func newCmdView() *cobra.Command { Short: "View teacher details", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - c, err := api.NewTypedClient(cmdutil.ServerFromCmd(cmd), false) - if err != nil { - return err - } - teacherID, err := strconv.ParseInt(args[0], 10, 64) - if err != nil { - return fmt.Errorf("invalid teacher id %q: %w", args[0], err) - } - data, err := api.ParseResponseRaw(c.GetTeacher(api.Ctx(), teacherID)) - if err != nil { - return err - } - if output.IsJSON() { - return output.JSON(data) - } - m := cmdutil.AsMap(data) - output.KVWithTitle([]output.KVPair{ - {Key: "ID", Value: output.Resolve(m, "id")}, - {Key: "Code", Value: output.Resolve(m, "code")}, - {Key: "Name", Value: output.Resolve(m, "namePrimary")}, - {Key: "Name (EN)", Value: output.Resolve(m, "nameSecondary")}, - {Key: "Department", Value: output.Resolve(m, "department.name")}, - {Key: "Title", Value: output.Resolve(m, "title")}, - }, "Teacher") - - if sections, ok := m["sections"].([]any); ok && len(sections) > 0 { - fmt.Println() - output.Bold(" Sections") - rows := cmdutil.RowsFromAny(sections) - output.Table(rows, []output.Column{ - {Header: "ID", Key: "id"}, - {Header: "Code", Key: "code"}, - {Header: "Course", Key: "course.namePrimary"}, - {Header: "Semester", Key: "semester.name"}, - }) - } - return nil + return runTeacherView(cmd, args[0]) }, } } + +func runTeacherView(cmd *cobra.Command, id string) error { + c, err := api.NewTypedClient(cmdutil.ServerFromCmd(cmd), false) + if err != nil { + return err + } + teacherID, err := strconv.ParseInt(id, 10, 64) + if err != nil { + return fmt.Errorf("invalid teacher id %q: %w", id, err) + } + data, err := api.ParseResponseRaw(c.GetTeacher(api.Ctx(), teacherID)) + if err != nil { + return err + } + if output.IsJSON() { + return output.JSON(data) + } + m := cmdutil.AsMap(data) + output.KVWithTitle([]output.KVPair{ + {Key: "ID", Value: output.Resolve(m, "id")}, + {Key: "Code", Value: output.Resolve(m, "code")}, + {Key: "Name", Value: output.Resolve(m, "namePrimary")}, + {Key: "Name (EN)", Value: output.Resolve(m, "nameSecondary")}, + {Key: "Department", Value: output.Resolve(m, "department.name")}, + {Key: "Title", Value: output.Resolve(m, "title")}, + }, "Teacher") + + if sections, ok := m["sections"].([]any); ok && len(sections) > 0 { + fmt.Println() + output.Bold(" Sections") + rows := cmdutil.RowsFromAny(sections) + output.Table(rows, []output.Column{ + {Header: "ID", Key: "id"}, + {Header: "Code", Key: "code"}, + {Header: "Course", Key: "course.namePrimary"}, + {Header: "Semester", Key: "semester.name"}, + }) + } + return nil +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 4eaac05..f93fcac 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -9,7 +9,9 @@ import ( "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/fatih/color" "github.com/mattn/go-runewidth" + "github.com/muesli/termenv" ) type SearchForm struct { @@ -36,6 +38,7 @@ type SearchTable struct { Form SearchForm Columns []output.Column Fetch func(SearchResult) (TableResult, error) + OnSelect func(map[string]any) error EmptyMessage string } @@ -48,17 +51,18 @@ const ( ) type tableModel struct { - spec SearchTable - inputs []textinput.Model - focus int - err string - mode tableMode - query SearchResult - result TableResult - selected int - scroll int - width int - height int + spec SearchTable + inputs []textinput.Model + focus int + err string + mode tableMode + query SearchResult + result TableResult + selected int + selection map[string]any + scroll int + width int + height int } type tableFetchMsg struct { @@ -70,9 +74,29 @@ func RunSearchTable(spec SearchTable) error { if spec.Fetch == nil { return fmt.Errorf("missing TUI fetch function") } + restoreColorProfile := configureSearchTableColors() + defer restoreColorProfile() + model := newTableModel(spec) - _, err := tea.NewProgram(model, tea.WithAltScreen()).Run() - return err + finalModel, err := tea.NewProgram(model, tea.WithAltScreen()).Run() + if err != nil { + return err + } + if result, ok := finalModel.(tableModel); ok && result.selection != nil && spec.OnSelect != nil { + return spec.OnSelect(result.selection) + } + return nil +} + +func configureSearchTableColors() func() { + if !output.Current.NoColor && !color.NoColor { + return func() {} + } + previous := lipgloss.ColorProfile() + lipgloss.SetColorProfile(termenv.Ascii) + return func() { + lipgloss.SetColorProfile(previous) + } } func newTableModel(spec SearchTable) tableModel { @@ -191,7 +215,16 @@ func (m tableModel) updateResults(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "ctrl+c", "esc", "q": return m, tea.Quit - case "/", "e", "enter": + case "/", "e": + m.mode = tableModeSearch + m.err = "" + m = m.focusInput(0) + return m, nil + case "enter": + if m.spec.OnSelect != nil && len(m.result.Rows) > 0 { + m.selection = m.result.Rows[m.selected] + return m, tea.Quit + } m.mode = tableModeSearch m.err = "" m = m.focusInput(0) @@ -288,7 +321,7 @@ func (m tableModel) searchView() string { } body := lipgloss.JoinVertical(lipgloss.Left, parts...) - return formBoxStyle.Width(contentWidth).Render(body) + return renderFormBox(body, contentWidth) } func (m tableModel) loadingView() string { @@ -302,7 +335,7 @@ func (m tableModel) loadingView() string { "", mutedStyle.Render("esc quit"), ) - return formBoxStyle.Width(contentWidth).Render(body) + return renderFormBox(body, contentWidth) } func (m tableModel) resultsView() string { @@ -324,10 +357,10 @@ func (m tableModel) resultsView() string { mutedStyle.Width(contentWidth).Render(helpText), ) body := lipgloss.JoinVertical(lipgloss.Left, parts...) - return formBoxStyle.Width(contentWidth).Render(body) + return renderFormBox(body, contentWidth) } - parts = append(parts, mutedStyle.Render(resultSummary(m.result))) + parts = append(parts, mutedStyle.Width(contentWidth).Render(resultSummary(m.result))) if len(m.result.Rows) == 0 { msg := m.spec.EmptyMessage if msg == "" { @@ -350,7 +383,15 @@ func (m tableModel) resultsView() string { parts = append(parts, "", mutedStyle.Width(contentWidth).Render(helpText)) } body := lipgloss.JoinVertical(lipgloss.Left, parts...) - return formBoxStyle.Width(contentWidth).Render(body) + return renderFormBox(body, contentWidth) +} + +func renderFormBox(body string, width int) string { + lines := strings.Split(body, "\n") + for i, line := range lines { + lines[i] = line + strings.Repeat(" ", max(0, width-lipgloss.Width(line))) + } + return formBoxStyle.Render(strings.Join(lines, "\n")) } func (m tableModel) renderTable(width int) string { @@ -366,7 +407,7 @@ func (m tableModel) renderTable(width int) string { absolute := m.scroll + i line := tableLine(cols, row, colWidths, gap, width, false) if absolute == m.selected { - line = selectedRowStyle.Width(width).Render(line) + line = selectedRowStyle.Render(line) } lines = append(lines, line) } @@ -427,20 +468,40 @@ func (m tableModel) canPageNext() bool { func (m tableModel) resultsHelpText(compact bool) string { if compact { + if m.spec.OnSelect != nil && len(m.result.Rows) > 0 { + if m.canPageNext() || m.currentPage() > 1 { + return "enter open • p/n • / edit • esc" + } + return "enter open • ↑/↓ • / edit • esc" + } if m.canPageNext() || m.currentPage() > 1 { return "p/n page • / edit • esc" } return "↑/↓ • r • / edit • esc" } - parts := []string{"↑/↓ move", "r refresh"} - if m.currentPage() > 1 { + if m.spec.OnSelect == nil || len(m.result.Rows) == 0 { + parts := []string{"↑/↓ move", "r refresh"} + if m.currentPage() > 1 { + parts = append(parts, "p prev") + } + if m.canPageNext() { + parts = append(parts, "n next") + } + parts = append(parts, "/ edit search", "esc quit") + return strings.Join(parts, " • ") + } + parts := make([]string, 0, 6) + parts = append(parts, "enter open") + parts = append(parts, "↑/↓ move", "r refresh") + if m.currentPage() > 1 && m.canPageNext() { + parts = append(parts, "p/n page") + } else if m.currentPage() > 1 { parts = append(parts, "p prev") - } - if m.canPageNext() { + } else if m.canPageNext() { parts = append(parts, "n next") } - parts = append(parts, "/ edit search", "esc quit") - return strings.Join(parts, " • ") + parts = append(parts, "/ edit", "esc quit") + return strings.Join(parts, " • ") } func (m tableModel) focusNext() tableModel { @@ -477,8 +538,6 @@ func (m tableModel) resultFromInputs() (SearchResult, error) { }, nil } - - func (m tableModel) searchContentWidth() int { return contentWidth(m.width, maxFormOuterWidth) } @@ -615,7 +674,6 @@ func tableCell(row map[string]any, col output.Column) string { return output.FormatCellPlain(output.Resolve(row, col.Key)) } - func padCell(value string, width int) string { if runewidth.StringWidth(value) > width { value = truncateCell(value, width) diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index 9811f57..8f92c1f 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -7,6 +7,8 @@ import ( "github.com/Life-USTC/CLI/internal/output" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/fatih/color" + "github.com/muesli/termenv" ) func TestSearchResultFromInputs(t *testing.T) { @@ -136,6 +138,53 @@ func TestSearchTableEditReturnsToSearchWithoutQuitting(t *testing.T) { } } +func TestSearchTableEnterSelectsHighlightedRow(t *testing.T) { + m := newTableModel(SearchTable{ + OnSelect: func(map[string]any) error { return nil }, + }) + m.mode = tableModeResults + m.selected = 1 + m.result.Rows = []map[string]any{ + {"jwId": float64(101)}, + {"jwId": float64(202)}, + } + + updated, cmd := m.Update(tea.KeyMsg{Type: tea.KeyEnter}) + m = updated.(tableModel) + if cmd == nil { + t.Fatal("enter should quit the TUI before opening the selected result") + } + if _, ok := cmd().(tea.QuitMsg); !ok { + t.Fatalf("enter command = %T, want tea.QuitMsg", cmd()) + } + if got := m.selection["jwId"]; got != float64(202) { + t.Fatalf("selected jwId = %v, want 202", got) + } +} + +func TestSearchTableNoColorUsesAsciiProfile(t *testing.T) { + previousProfile := lipgloss.ColorProfile() + previousNoColor := output.Current.NoColor + previousFatihNoColor := color.NoColor + lipgloss.SetColorProfile(termenv.ANSI256) + output.Current.NoColor = true + color.NoColor = false + t.Cleanup(func() { + lipgloss.SetColorProfile(previousProfile) + output.Current.NoColor = previousNoColor + color.NoColor = previousFatihNoColor + }) + + restore := configureSearchTableColors() + if got := lipgloss.ColorProfile(); got != termenv.Ascii { + t.Fatalf("color profile = %v, want Ascii", got) + } + restore() + if got := lipgloss.ColorProfile(); got != termenv.ANSI256 { + t.Fatalf("restored color profile = %v, want ANSI256", got) + } +} + func TestSearchTableCanPageInsideResults(t *testing.T) { var got SearchResult m := newTableModel(SearchTable{ @@ -183,6 +232,7 @@ func TestSearchTableResultsFitCompactWindow(t *testing.T) { Fetch: func(SearchResult) (TableResult, error) { return TableResult{}, nil }, + OnSelect: func(map[string]any) error { return nil }, }) updated, _ := m.Update(tea.WindowSizeMsg{Width: 40, Height: 12}) m = updated.(tableModel) @@ -208,6 +258,69 @@ func TestSearchTableResultsFitCompactWindow(t *testing.T) { if renderedHeight := lipgloss.Height(rendered); renderedHeight > 12 { t.Fatalf("rendered height = %d, want <= 12\n%s", renderedHeight, rendered) } + if !strings.Contains(rendered, "enter open") { + t.Fatalf("compact results did not show the selection action:\n%s", rendered) + } +} + +func TestSearchTableSelectedCJKRowDoesNotWrapWithColor(t *testing.T) { + previousProfile := lipgloss.ColorProfile() + lipgloss.SetColorProfile(termenv.ANSI256) + t.Cleanup(func() { + lipgloss.SetColorProfile(previousProfile) + }) + + m := newTableModel(SearchTable{ + Columns: []output.Column{ + {Header: "Code", Key: "code"}, + {Header: "Name", Key: "namePrimary"}, + {Header: "Level", Key: "educationLevel.name"}, + {Header: "JW ID", Key: "jwId"}, + }, + OnSelect: func(map[string]any) error { return nil }, + }) + m.mode = tableModeResults + m.result = TableResult{ + Rows: []map[string]any{ + { + "code": "MATH101", + "namePrimary": "数学分析(B1)", + "educationLevel": map[string]any{"name": "Undergraduate"}, + "jwId": "course-jw-id-1", + }, + { + "code": "MATH102", + "namePrimary": "线性代数(A)", + "educationLevel": map[string]any{"name": "Undergraduate"}, + "jwId": "course-jw-id-2", + }, + }, + Total: 2, + Page: 1, + } + + for _, size := range []struct { + width int + height int + }{ + {width: 80, height: 24}, + {width: 40, height: 12}, + } { + updated, _ := m.Update(tea.WindowSizeMsg{Width: size.width, Height: size.height}) + sized := updated.(tableModel) + selected := sized.View() + sized.selected = len(sized.result.Rows) + unselected := sized.View() + if selectedHeight, unselectedHeight := lipgloss.Height(selected), lipgloss.Height(unselected); selectedHeight != unselectedHeight { + t.Fatalf("%dx%d selected view height = %d, unselected view height = %d\nselected:\n%s\nunselected:\n%s", size.width, size.height, selectedHeight, unselectedHeight, selected, unselected) + } + if selectedHeight := lipgloss.Height(selected); selectedHeight > size.height { + t.Fatalf("%dx%d selected view height = %d\n%s", size.width, size.height, selectedHeight, selected) + } + if selectedWidth := lipgloss.Width(selected); selectedWidth > size.width { + t.Fatalf("%dx%d selected view width = %d\n%s", size.width, size.height, selectedWidth, selected) + } + } } func TestSearchTableErrorFitsCompactWindow(t *testing.T) {