From 329ac72275fa152b874714c2b28a18e95f0da6fb Mon Sep 17 00:00:00 2001 From: "m@yim.jp" Date: Mon, 12 Jan 2026 06:54:45 +0000 Subject: [PATCH 1/6] feat(view): add :settings command to display config (#145) - Add ModalWidthSettings = 70 constant - Create settings_view.go with 10 sections: Config File, Runtime, Startup, Theme, Timeouts, Concurrency, CloudWatch, Navigation, Autosave, AI - Add :settings command handler and autocomplete - Show CLI overrides with (CLI) suffix - Scrollable with j/k and mouse - Add Ctrl+C to close modals globally --- internal/app/app.go | 2 +- internal/view/command_input.go | 16 ++ internal/view/modal.go | 1 + internal/view/settings_view.go | 363 +++++++++++++++++++++++++++++++++ 4 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 internal/view/settings_view.go diff --git a/internal/app/app.go b/internal/app/app.go index 2ec80d9..c836c08 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -599,7 +599,7 @@ func (a *App) handleModalUpdate(msg tea.Msg) (tea.Model, tea.Cmd) { return a.handleProfilesChanged(msg) case tea.KeyPressMsg: - if view.IsEscKey(msg) || msg.Code == tea.KeyBackspace || msg.String() == "q" { + if view.IsEscKey(msg) || msg.Code == tea.KeyBackspace || msg.String() == "q" || msg.String() == "ctrl+c" { if ic, ok := a.modal.Content.(view.InputCapture); ok && ic.HasActiveInput() { break } diff --git a/internal/view/command_input.go b/internal/view/command_input.go index 87af7a0..6e02f5c 100644 --- a/internal/view/command_input.go +++ b/internal/view/command_input.go @@ -474,6 +474,18 @@ func (c *CommandInput) executeCommand() (tea.Cmd, *NavigateMsg) { return nil, &NavigateMsg{View: browser, ClearStack: false} } + // Handle settings command - show settings modal + if input == "settings" { + return func() tea.Msg { + return ShowModalMsg{ + Modal: &Modal{ + Content: NewSettingsView(c.ctx), + Width: ModalWidthSettings, + }, + } + }, nil + } + // Handle sort command: :sort (clear) or :sort (sort by column) if input == "sort" { return func() tea.Msg { @@ -704,6 +716,10 @@ func (c *CommandInput) GetSuggestions() []string { suggestions = append(suggestions, "autosave") } + if strings.HasPrefix("settings", input) { + suggestions = append(suggestions, "settings") + } + for _, svc := range c.registry.ListServices() { // Skip if input exactly matches service (already fully typed) if svc != input && strings.HasPrefix(svc, input) { diff --git a/internal/view/modal.go b/internal/view/modal.go index 3700b80..60063c7 100644 --- a/internal/view/modal.go +++ b/internal/view/modal.go @@ -23,6 +23,7 @@ const ( ModalWidthProfile = 55 ModalWidthProfileDetail = 65 ModalWidthActionMenu = 60 + ModalWidthSettings = 70 ModalWidthChat = 80 ) diff --git a/internal/view/settings_view.go b/internal/view/settings_view.go new file mode 100644 index 0000000..11f1245 --- /dev/null +++ b/internal/view/settings_view.go @@ -0,0 +1,363 @@ +package view + +import ( + "context" + "fmt" + "strings" + + tea "charm.land/bubbletea/v2" + + "github.com/clawscli/claws/internal/config" + "github.com/clawscli/claws/internal/ui" +) + +const ( + noneValue = "(none)" + settingsSeparatorInset = 10 +) + +type SettingsView struct { + ctx context.Context + vp ViewportState + screenWidth int + screenHeight int +} + +func NewSettingsView(ctx context.Context) *SettingsView { + return &SettingsView{ + ctx: ctx, + } +} + +func (v *SettingsView) Init() tea.Cmd { + return nil +} + +func (v *SettingsView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + v.vp.Model, cmd = v.vp.Model.Update(msg) + return v, cmd +} + +func (v *SettingsView) View() tea.View { + return tea.NewView(v.ViewString()) +} + +func (v *SettingsView) ViewString() string { + if !v.vp.Ready { + return LoadingMessage + } + return v.vp.Model.View() +} + +func (v *SettingsView) StatusLine() string { + return "" +} + +func (v *SettingsView) SetSize(w, h int) tea.Cmd { + v.screenWidth, v.screenHeight = w, h + content := v.buildContent() + v.vp.SetSize(w, h) + v.vp.Model.SetContent(content) + return nil +} + +func (v *SettingsView) buildContent() string { + var sb strings.Builder + cfg := config.File() + globalCfg := config.Global() + + separatorWidth := ModalWidthSettings - settingsSeparatorInset + separator := ui.DimStyle().Render(" " + strings.Repeat("─", separatorWidth)) + + // Section 1: Config File + sb.WriteString(ui.TitleStyle().Render("Config File")) + sb.WriteString("\n\n") + configPath := config.GetConfigPath() + if configPath != "" { + sb.WriteString(fmt.Sprintf(" Path %s\n", configPath)) + sb.WriteString(" Type custom\n") + } else { + sb.WriteString(" Path ~/.config/claws/config.yaml (default)\n") + sb.WriteString(" Type default\n") + } + sb.WriteString("\n") + sb.WriteString(separator) + sb.WriteString("\n\n") + + // Section 2: Runtime + sb.WriteString(ui.TitleStyle().Render("Runtime")) + sb.WriteString("\n\n") + + // Regions + runtimeRegions := globalCfg.Regions() + startupRegions := cfg.Startup.Regions + regionsMatch := v.slicesEqual(runtimeRegions, startupRegions) + regionStr := strings.Join(runtimeRegions, ", ") + if regionStr == "" { + regionStr = noneValue + } + if !regionsMatch && len(startupRegions) > 0 { + regionStr += " (CLI)" + } + sb.WriteString(fmt.Sprintf(" Regions %s\n", regionStr)) + + // Profiles + runtimeProfiles := v.getProfileIDs(globalCfg.Selections()) + startupProfiles := cfg.Startup.GetProfiles() + profilesMatch := v.slicesEqual(runtimeProfiles, startupProfiles) + profileStr := v.formatProfiles(globalCfg.Selections()) + if !profilesMatch && len(startupProfiles) > 0 { + profileStr += " (CLI)" + } + sb.WriteString(fmt.Sprintf(" Profiles %s\n", profileStr)) + + // Read-only + readOnly := "no" + if globalCfg.ReadOnly() { + readOnly = "yes" + } + sb.WriteString(fmt.Sprintf(" Read-only %s\n", readOnly)) + + sb.WriteString("\n") + sb.WriteString(separator) + sb.WriteString("\n\n") + + // Section 3: Startup + sb.WriteString(ui.TitleStyle().Render("Startup")) + sb.WriteString("\n\n") + + view := cfg.GetStartupView() + if view == "" { + view = noneValue + } + sb.WriteString(fmt.Sprintf(" View %s\n", view)) + + startupRegionStr := strings.Join(cfg.Startup.Regions, ", ") + if startupRegionStr == "" { + startupRegionStr = noneValue + } + sb.WriteString(fmt.Sprintf(" Regions %s\n", startupRegionStr)) + + startupProfileStr := strings.Join(cfg.Startup.GetProfiles(), ", ") + if startupProfileStr == "" { + startupProfileStr = noneValue + } + sb.WriteString(fmt.Sprintf(" Profiles %s\n", startupProfileStr)) + + sb.WriteString("\n") + sb.WriteString(separator) + sb.WriteString("\n\n") + + // Section 4: Theme + sb.WriteString(ui.TitleStyle().Render("Theme")) + sb.WriteString("\n\n") + + theme := cfg.GetTheme() + preset := theme.Preset + if preset == "" { + preset = noneValue + } + sb.WriteString(fmt.Sprintf(" Preset %s\n", preset)) + + // Show overridden colors + overrides := v.getThemeOverrides(theme) + if len(overrides) > 0 { + sb.WriteString("\n") + for _, override := range overrides { + sb.WriteString(fmt.Sprintf(" + %s\n", override)) + } + } + + sb.WriteString("\n") + sb.WriteString(separator) + sb.WriteString("\n\n") + + // Section 5: Timeouts + sb.WriteString(ui.TitleStyle().Render("Timeouts")) + sb.WriteString("\n\n") + sb.WriteString(fmt.Sprintf(" AWS Init %s\n", cfg.Timeouts.AWSInit.Duration().String())) + sb.WriteString(fmt.Sprintf(" Multi-region %s\n", cfg.Timeouts.MultiRegionFetch.Duration().String())) + sb.WriteString(fmt.Sprintf(" Tag search %s\n", cfg.Timeouts.TagSearch.Duration().String())) + sb.WriteString(fmt.Sprintf(" Metrics load %s\n", cfg.Timeouts.MetricsLoad.Duration().String())) + sb.WriteString(fmt.Sprintf(" Log fetch %s\n", cfg.Timeouts.LogFetch.Duration().String())) + sb.WriteString(fmt.Sprintf(" Docs search %s\n", cfg.Timeouts.DocsSearch.Duration().String())) + sb.WriteString("\n") + sb.WriteString(separator) + sb.WriteString("\n\n") + + // Section 6: Concurrency + sb.WriteString(ui.TitleStyle().Render("Concurrency")) + sb.WriteString("\n\n") + sb.WriteString(fmt.Sprintf(" Max fetches %d\n", cfg.Concurrency.MaxFetches)) + sb.WriteString("\n") + sb.WriteString(separator) + sb.WriteString("\n\n") + + // Section 7: CloudWatch + sb.WriteString(ui.TitleStyle().Render("CloudWatch")) + sb.WriteString("\n\n") + sb.WriteString(fmt.Sprintf(" Metrics window %s\n", cfg.CloudWatch.Window.Duration().String())) + sb.WriteString("\n") + sb.WriteString(separator) + sb.WriteString("\n\n") + + // Section 8: Navigation + sb.WriteString(ui.TitleStyle().Render("Navigation")) + sb.WriteString("\n\n") + sb.WriteString(fmt.Sprintf(" Max stack size %d\n", cfg.Navigation.MaxStackSize)) + sb.WriteString("\n") + sb.WriteString(separator) + sb.WriteString("\n\n") + + // Section 9: Autosave + sb.WriteString(ui.TitleStyle().Render("Autosave")) + sb.WriteString("\n\n") + enabled := "no" + if cfg.Autosave.Enabled { + enabled = "yes" + } + sb.WriteString(fmt.Sprintf(" Enabled %s\n", enabled)) + sb.WriteString("\n") + sb.WriteString(separator) + sb.WriteString("\n\n") + + // Section 10: AI + sb.WriteString(ui.TitleStyle().Render("AI")) + sb.WriteString("\n\n") + + aiProfile := cfg.AI.Profile + if aiProfile == "" { + aiProfile = "(default)" + } + sb.WriteString(fmt.Sprintf(" Profile %s\n", aiProfile)) + + aiRegion := cfg.AI.Region + if aiRegion == "" { + aiRegion = "(default)" + } + sb.WriteString(fmt.Sprintf(" Region %s\n", aiRegion)) + + sb.WriteString(fmt.Sprintf(" Model %s\n", cfg.GetAIModel())) + sb.WriteString(fmt.Sprintf(" Max sessions %d\n", cfg.GetAIMaxSessions())) + sb.WriteString(fmt.Sprintf(" Max tokens %d\n", cfg.GetAIMaxTokens())) + sb.WriteString(fmt.Sprintf(" Thinking budget %d\n", cfg.GetAIThinkingBudget())) + sb.WriteString(fmt.Sprintf(" Max tool rounds %d\n", cfg.GetAIMaxToolRounds())) + sb.WriteString(fmt.Sprintf(" Max tool calls %d\n", cfg.GetAIMaxToolCallsPerQuery())) + + saveSessions := "no" + if cfg.GetAISaveSessions() { + saveSessions = "yes" + } + sb.WriteString(fmt.Sprintf(" Save sessions %s\n", saveSessions)) + + return sb.String() +} + +func (v *SettingsView) getThemeOverrides(theme config.ThemeConfig) []string { + var overrides []string + + if theme.Primary != "" { + overrides = append(overrides, fmt.Sprintf("Primary: %s", theme.Primary)) + } + if theme.Secondary != "" { + overrides = append(overrides, fmt.Sprintf("Secondary: %s", theme.Secondary)) + } + if theme.Accent != "" { + overrides = append(overrides, fmt.Sprintf("Accent: %s", theme.Accent)) + } + if theme.Text != "" { + overrides = append(overrides, fmt.Sprintf("Text: %s", theme.Text)) + } + if theme.TextBright != "" { + overrides = append(overrides, fmt.Sprintf("TextBright: %s", theme.TextBright)) + } + if theme.TextDim != "" { + overrides = append(overrides, fmt.Sprintf("TextDim: %s", theme.TextDim)) + } + if theme.TextMuted != "" { + overrides = append(overrides, fmt.Sprintf("TextMuted: %s", theme.TextMuted)) + } + if theme.Success != "" { + overrides = append(overrides, fmt.Sprintf("Success: %s", theme.Success)) + } + if theme.Warning != "" { + overrides = append(overrides, fmt.Sprintf("Warning: %s", theme.Warning)) + } + if theme.Danger != "" { + overrides = append(overrides, fmt.Sprintf("Danger: %s", theme.Danger)) + } + if theme.Info != "" { + overrides = append(overrides, fmt.Sprintf("Info: %s", theme.Info)) + } + if theme.Pending != "" { + overrides = append(overrides, fmt.Sprintf("Pending: %s", theme.Pending)) + } + if theme.Border != "" { + overrides = append(overrides, fmt.Sprintf("Border: %s", theme.Border)) + } + if theme.BorderHighlight != "" { + overrides = append(overrides, fmt.Sprintf("BorderHighlight: %s", theme.BorderHighlight)) + } + if theme.Background != "" { + overrides = append(overrides, fmt.Sprintf("Background: %s", theme.Background)) + } + if theme.BackgroundAlt != "" { + overrides = append(overrides, fmt.Sprintf("BackgroundAlt: %s", theme.BackgroundAlt)) + } + if theme.Selection != "" { + overrides = append(overrides, fmt.Sprintf("Selection: %s", theme.Selection)) + } + if theme.SelectionText != "" { + overrides = append(overrides, fmt.Sprintf("SelectionText: %s", theme.SelectionText)) + } + if theme.TableHeader != "" { + overrides = append(overrides, fmt.Sprintf("TableHeader: %s", theme.TableHeader)) + } + if theme.TableHeaderText != "" { + overrides = append(overrides, fmt.Sprintf("TableHeaderText: %s", theme.TableHeaderText)) + } + if theme.TableBorder != "" { + overrides = append(overrides, fmt.Sprintf("TableBorder: %s", theme.TableBorder)) + } + if theme.BadgeForeground != "" { + overrides = append(overrides, fmt.Sprintf("BadgeForeground: %s", theme.BadgeForeground)) + } + if theme.BadgeBackground != "" { + overrides = append(overrides, fmt.Sprintf("BadgeBackground: %s", theme.BadgeBackground)) + } + + return overrides +} + +func (v *SettingsView) formatProfiles(selections []config.ProfileSelection) string { + if len(selections) == 0 { + return noneValue + } + + names := make([]string, len(selections)) + for i, sel := range selections { + names[i] = sel.DisplayName() + } + return strings.Join(names, ", ") +} + +func (v *SettingsView) getProfileIDs(selections []config.ProfileSelection) []string { + ids := make([]string, len(selections)) + for i, sel := range selections { + ids[i] = sel.ID() + } + return ids +} + +func (v *SettingsView) slicesEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} From e6acba55f21336c8a29e49df9dfc56fcb2a34a32 Mon Sep 17 00:00:00 2001 From: "m@yim.jp" Date: Tue, 13 Jan 2026 08:57:01 +0000 Subject: [PATCH 2/6] chore: ignore .sisyphus directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6c92ad6..278435e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ Thumbs.db CLAUDE.md AGENTS.md .serena/ +.sisyphus/ # Build artifacts dist/ From 0609cf1af39467f9c63157eabe6e3b7e28564538 Mon Sep 17 00:00:00 2001 From: "m@yim.jp" Date: Tue, 13 Jan 2026 09:07:20 +0000 Subject: [PATCH 3/6] fix(view): add style caching and nil-safe Update to SettingsView --- internal/view/settings_view.go | 53 ++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/internal/view/settings_view.go b/internal/view/settings_view.go index 11f1245..0933d78 100644 --- a/internal/view/settings_view.go +++ b/internal/view/settings_view.go @@ -6,6 +6,7 @@ import ( "strings" tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/clawscli/claws/internal/config" "github.com/clawscli/claws/internal/ui" @@ -16,16 +17,31 @@ const ( settingsSeparatorInset = 10 ) +// settingsViewStyles holds cached lipgloss styles for performance. +type settingsViewStyles struct { + title lipgloss.Style + separator lipgloss.Style +} + +func newSettingsViewStyles() settingsViewStyles { + return settingsViewStyles{ + title: ui.TitleStyle(), + separator: ui.DimStyle(), + } +} + type SettingsView struct { ctx context.Context vp ViewportState screenWidth int screenHeight int + styles settingsViewStyles } func NewSettingsView(ctx context.Context) *SettingsView { return &SettingsView{ - ctx: ctx, + ctx: ctx, + styles: newSettingsViewStyles(), } } @@ -34,6 +50,19 @@ func (v *SettingsView) Init() tea.Cmd { } func (v *SettingsView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg.(type) { + case ThemeChangedMsg: + v.styles = newSettingsViewStyles() + if v.vp.Ready { + v.vp.Model.SetContent(v.buildContent()) + } + return v, nil + } + + if !v.vp.Ready { + return v, nil + } + var cmd tea.Cmd v.vp.Model, cmd = v.vp.Model.Update(msg) return v, cmd @@ -68,10 +97,10 @@ func (v *SettingsView) buildContent() string { globalCfg := config.Global() separatorWidth := ModalWidthSettings - settingsSeparatorInset - separator := ui.DimStyle().Render(" " + strings.Repeat("─", separatorWidth)) + separator := v.styles.separator.Render(" " + strings.Repeat("─", separatorWidth)) // Section 1: Config File - sb.WriteString(ui.TitleStyle().Render("Config File")) + sb.WriteString(v.styles.title.Render("Config File")) sb.WriteString("\n\n") configPath := config.GetConfigPath() if configPath != "" { @@ -86,7 +115,7 @@ func (v *SettingsView) buildContent() string { sb.WriteString("\n\n") // Section 2: Runtime - sb.WriteString(ui.TitleStyle().Render("Runtime")) + sb.WriteString(v.styles.title.Render("Runtime")) sb.WriteString("\n\n") // Regions @@ -124,7 +153,7 @@ func (v *SettingsView) buildContent() string { sb.WriteString("\n\n") // Section 3: Startup - sb.WriteString(ui.TitleStyle().Render("Startup")) + sb.WriteString(v.styles.title.Render("Startup")) sb.WriteString("\n\n") view := cfg.GetStartupView() @@ -150,7 +179,7 @@ func (v *SettingsView) buildContent() string { sb.WriteString("\n\n") // Section 4: Theme - sb.WriteString(ui.TitleStyle().Render("Theme")) + sb.WriteString(v.styles.title.Render("Theme")) sb.WriteString("\n\n") theme := cfg.GetTheme() @@ -174,7 +203,7 @@ func (v *SettingsView) buildContent() string { sb.WriteString("\n\n") // Section 5: Timeouts - sb.WriteString(ui.TitleStyle().Render("Timeouts")) + sb.WriteString(v.styles.title.Render("Timeouts")) sb.WriteString("\n\n") sb.WriteString(fmt.Sprintf(" AWS Init %s\n", cfg.Timeouts.AWSInit.Duration().String())) sb.WriteString(fmt.Sprintf(" Multi-region %s\n", cfg.Timeouts.MultiRegionFetch.Duration().String())) @@ -187,7 +216,7 @@ func (v *SettingsView) buildContent() string { sb.WriteString("\n\n") // Section 6: Concurrency - sb.WriteString(ui.TitleStyle().Render("Concurrency")) + sb.WriteString(v.styles.title.Render("Concurrency")) sb.WriteString("\n\n") sb.WriteString(fmt.Sprintf(" Max fetches %d\n", cfg.Concurrency.MaxFetches)) sb.WriteString("\n") @@ -195,7 +224,7 @@ func (v *SettingsView) buildContent() string { sb.WriteString("\n\n") // Section 7: CloudWatch - sb.WriteString(ui.TitleStyle().Render("CloudWatch")) + sb.WriteString(v.styles.title.Render("CloudWatch")) sb.WriteString("\n\n") sb.WriteString(fmt.Sprintf(" Metrics window %s\n", cfg.CloudWatch.Window.Duration().String())) sb.WriteString("\n") @@ -203,7 +232,7 @@ func (v *SettingsView) buildContent() string { sb.WriteString("\n\n") // Section 8: Navigation - sb.WriteString(ui.TitleStyle().Render("Navigation")) + sb.WriteString(v.styles.title.Render("Navigation")) sb.WriteString("\n\n") sb.WriteString(fmt.Sprintf(" Max stack size %d\n", cfg.Navigation.MaxStackSize)) sb.WriteString("\n") @@ -211,7 +240,7 @@ func (v *SettingsView) buildContent() string { sb.WriteString("\n\n") // Section 9: Autosave - sb.WriteString(ui.TitleStyle().Render("Autosave")) + sb.WriteString(v.styles.title.Render("Autosave")) sb.WriteString("\n\n") enabled := "no" if cfg.Autosave.Enabled { @@ -223,7 +252,7 @@ func (v *SettingsView) buildContent() string { sb.WriteString("\n\n") // Section 10: AI - sb.WriteString(ui.TitleStyle().Render("AI")) + sb.WriteString(v.styles.title.Render("AI")) sb.WriteString("\n\n") aiProfile := cfg.AI.Profile From bca296eea7594c965111d4ca83eb83d28ea5f44a Mon Sep 17 00:00:00 2001 From: "m@yim.jp" Date: Tue, 13 Jan 2026 09:32:00 +0000 Subject: [PATCH 4/6] docs(view): add :settings to help documentation --- internal/view/help_view.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/view/help_view.go b/internal/view/help_view.go index e728ff0..bbc4959 100644 --- a/internal/view/help_view.go +++ b/internal/view/help_view.go @@ -110,6 +110,7 @@ func (h *HelpView) renderContent() string { out += s.key.Render(":login ") + s.desc.Render("AWS Console login with profile") + "\n" out += s.key.Render(":theme ") + s.desc.Render("Change theme (dark/light/nord/dracula/...)") + "\n" out += s.key.Render(":autosave") + s.desc.Render("Toggle config persistence (on/off)") + "\n" + out += s.key.Render(":settings") + s.desc.Render("Show current settings") + "\n" // Tag Commands out += "\n" + s.section.Render("Tag Commands") + "\n" From 7eebd3ee97e99879e01ee76b034ded344cf51e09 Mon Sep 17 00:00:00 2001 From: "m@yim.jp" Date: Tue, 13 Jan 2026 09:46:25 +0000 Subject: [PATCH 5/6] refactor(view): use slices.Equal instead of custom method --- internal/view/settings_view.go | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/internal/view/settings_view.go b/internal/view/settings_view.go index 0933d78..3d79405 100644 --- a/internal/view/settings_view.go +++ b/internal/view/settings_view.go @@ -3,6 +3,7 @@ package view import ( "context" "fmt" + "slices" "strings" tea "charm.land/bubbletea/v2" @@ -121,7 +122,7 @@ func (v *SettingsView) buildContent() string { // Regions runtimeRegions := globalCfg.Regions() startupRegions := cfg.Startup.Regions - regionsMatch := v.slicesEqual(runtimeRegions, startupRegions) + regionsMatch := slices.Equal(runtimeRegions, startupRegions) regionStr := strings.Join(runtimeRegions, ", ") if regionStr == "" { regionStr = noneValue @@ -134,7 +135,7 @@ func (v *SettingsView) buildContent() string { // Profiles runtimeProfiles := v.getProfileIDs(globalCfg.Selections()) startupProfiles := cfg.Startup.GetProfiles() - profilesMatch := v.slicesEqual(runtimeProfiles, startupProfiles) + profilesMatch := slices.Equal(runtimeProfiles, startupProfiles) profileStr := v.formatProfiles(globalCfg.Selections()) if !profilesMatch && len(startupProfiles) > 0 { profileStr += " (CLI)" @@ -378,15 +379,3 @@ func (v *SettingsView) getProfileIDs(selections []config.ProfileSelection) []str } return ids } - -func (v *SettingsView) slicesEqual(a, b []string) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i] != b[i] { - return false - } - } - return true -} From 368063bc1adabce59366d5f0765979080086567d Mon Sep 17 00:00:00 2001 From: "m@yim.jp" Date: Tue, 13 Jan 2026 09:54:05 +0000 Subject: [PATCH 6/6] test(view): add SettingsView unit tests --- internal/view/settings_view_test.go | 80 +++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 internal/view/settings_view_test.go diff --git a/internal/view/settings_view_test.go b/internal/view/settings_view_test.go new file mode 100644 index 0000000..f22530c --- /dev/null +++ b/internal/view/settings_view_test.go @@ -0,0 +1,80 @@ +package view + +import ( + "context" + "testing" +) + +func TestSettingsView_New(t *testing.T) { + sv := NewSettingsView(context.Background()) + + if sv == nil { + t.Fatal("NewSettingsView() returned nil") + } +} + +func TestSettingsView_Init(t *testing.T) { + sv := NewSettingsView(context.Background()) + + cmd := sv.Init() + if cmd != nil { + t.Error("Init() should return nil") + } +} + +func TestSettingsView_StatusLine(t *testing.T) { + sv := NewSettingsView(context.Background()) + + status := sv.StatusLine() + if status != "" { + t.Error("StatusLine() should return empty string for modal") + } +} + +func TestSettingsView_ViewString_BeforeSetSize(t *testing.T) { + sv := NewSettingsView(context.Background()) + + view := sv.ViewString() + if view != LoadingMessage { + t.Errorf("ViewString() before SetSize should return LoadingMessage, got: %s", view) + } +} + +func TestSettingsView_SetSize(t *testing.T) { + sv := NewSettingsView(context.Background()) + + cmd := sv.SetSize(80, 24) + if cmd != nil { + t.Error("SetSize() should return nil") + } + + if !sv.vp.Ready { + t.Error("viewport should be ready after SetSize") + } + + view := sv.ViewString() + if view == LoadingMessage { + t.Error("ViewString() after SetSize should not return LoadingMessage") + } +} + +func TestSettingsView_ThemeChanged(t *testing.T) { + sv := NewSettingsView(context.Background()) + sv.SetSize(80, 24) + + originalStyles := sv.styles + + model, cmd := sv.Update(ThemeChangedMsg{}) + if cmd != nil { + t.Error("Update(ThemeChangedMsg) should return nil cmd") + } + + updated, ok := model.(*SettingsView) + if !ok { + t.Fatal("Update should return *SettingsView") + } + + if &updated.styles == &originalStyles { + t.Error("styles should be regenerated on ThemeChangedMsg") + } +}