From 18ed37a3e534bc1c6cfb67ecc3f65831db66099e Mon Sep 17 00:00:00 2001 From: Ting Kai Chiu <60634933+tingkai-c@users.noreply.github.com> Date: Fri, 1 May 2026 12:02:48 +0800 Subject: [PATCH] tui: make styled panel responsive to terminal width --- internal/tui/tui_list.go | 71 ++++++++++++++++++++++++++++++++++++---- internal/tui/tui_test.go | 22 +++++++++++++ 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/internal/tui/tui_list.go b/internal/tui/tui_list.go index 441e83c..52bdbee 100644 --- a/internal/tui/tui_list.go +++ b/internal/tui/tui_list.go @@ -2,11 +2,13 @@ package tui import ( "fmt" + "strings" "time" "github.com/tingkai-c/localsend-cli/internal/models" bubbletea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) // selectDevice 使用 Bubble Tea 库显示可供选择的设备列表并等待用户选择 @@ -59,8 +61,39 @@ type model struct { sortedKeys []string // 保持固定的显示顺序 cursor int updates <-chan []models.SendModel + width int } +var ( + appStyle = lipgloss.NewStyle(). + Padding(1, 2). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("63")) + + titleStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("86")) + + subtitleStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("245")) + + selectedStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("230")). + Background(lipgloss.Color("63")). + Padding(0, 1) + + normalStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("252")) + + emptyStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("214")) + // Slight emphasis for loading state. + + helpStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("244")) +) + // TickMsg 用于定期触发更新 type TickMsg time.Time @@ -128,24 +161,48 @@ func (m model) Update(msg bubbletea.Msg) (bubbletea.Model, bubbletea.Cmd) { default: } return m, tick() + case bubbletea.WindowSizeMsg: + m.width = msg.Width } return m, nil } // View 实现 Bubble Tea 的 View 方法 func (m model) View() string { + panel := appStyle + if m.width > 0 { + maxWidth := m.width - 2 + if maxWidth > 32 { + panel = panel.MaxWidth(maxWidth) + } + } + if len(m.devices) == 0 { - return "Scanning Devices...\n\n Press Ctrl+C to exit" + body := strings.Join([]string{ + titleStyle.Render("LocalSend"), + "", + emptyStyle.Render("⏳ Scanning for nearby devices..."), + "", + helpStyle.Render("Press Ctrl+C to exit"), + }, "\n") + return panel.Render(body) + } + + lines := []string{ + titleStyle.Render("LocalSend"), + subtitleStyle.Render(fmt.Sprintf("Found Devices • %d online", len(m.devices))), + "", } - s := "Found Devices:\n\n" for i, device := range m.devices { - cursor := " " // 默认没有光标 + line := fmt.Sprintf(" %s %s", device.DeviceName, subtitleStyle.Render(device.IP)) if m.cursor == i { - cursor = ">" // 选中的光标 + lines = append(lines, selectedStyle.Render("› "+line)) + continue } - s += fmt.Sprintf("%s %s (%s)\n", cursor, device.DeviceName, device.IP) + lines = append(lines, normalStyle.Render(" "+line)) } - s += "\nUse arrow keys to navigate and enter to select. Press Ctrl+C to exit." - return s + + lines = append(lines, "", helpStyle.Render("↑/↓ or j/k: navigate • Enter: select • Ctrl+C: quit")) + return panel.Render(strings.Join(lines, "\n")) } diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index 11e6a61..bb042bd 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -2,6 +2,7 @@ package tui import ( "os" + "strings" "testing" "time" @@ -51,3 +52,24 @@ func TestSelectDevice(t *testing.T) { t.Fatalf("SelectDevice returned an unexpected IP: %s", ip) } } + +func TestViewPreview(t *testing.T) { + m := model{ + devices: []models.SendModel{ + {IP: "192.168.1.10", DeviceName: "MacBook Pro"}, + {IP: "192.168.1.22", DeviceName: "Steam Deck"}, + {IP: "192.168.1.35", DeviceName: "Pixel 9"}, + }, + cursor: 1, + } + + view := m.View() + if !strings.Contains(view, "LocalSend") { + t.Fatalf("expected title in view, got: %q", view) + } + if !strings.Contains(view, "Steam Deck") { + t.Fatalf("expected selected device in view, got: %q", view) + } + + t.Logf("TUI preview:\n%s", view) +}