Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e6a1556
feat(config): add compact_header setting
yimsk Jan 13, 2026
24bd48a
refactor(header): unified 5-line normal mode layout
yimsk Jan 13, 2026
e398144
feat(header): add compact mode (1-line)
yimsk Jan 13, 2026
11a47f4
feat(views): add Ctrl+E header toggle
yimsk Jan 13, 2026
63fa02e
test(header): add tests for compact mode
yimsk Jan 13, 2026
ac06cdb
fix(header): simplify RenderHome to 2-line header
yimsk Jan 13, 2026
b139b74
fix(header): ctrl+e toggle layout issues
yimsk Jan 13, 2026
41df4d5
refactor(header): centralize ctrl+e, fix styling and theme updates
yimsk Jan 14, 2026
2a260a2
refactor(header): extract constants and deduplicate profile formatting
yimsk Jan 14, 2026
cf97b53
fix(header): recalc viewport on compact toggle, persist with autosave
yimsk Jan 14, 2026
7abe502
fix(header): dynamic profile display width and ANSI style reset
yimsk Jan 14, 2026
14d336c
fix(header): review fixes - off-by-one, regions truncation, settings/…
yimsk Jan 14, 2026
e5aa323
feat(cli): add --no-compact flag, fix light theme text colors
yimsk Jan 14, 2026
c673c01
fix(view): add viewport ready guard, move TextInputStyles to ui pkg
yimsk Jan 14, 2026
4d95ae6
chore(view): cleanup from review - rename label, add comments, remove…
yimsk Jan 14, 2026
fa5d3cf
fix: review issues - test expectation, dashboard compact handler, docs
yimsk Jan 14, 2026
55e4a1e
fix(header): truncate regions/profiles on invalid width
yimsk Jan 14, 2026
6033929
fix(view): remove confusing (CLI) indicators from settings view
yimsk Jan 14, 2026
4e45f83
refactor(view): extract magic numbers to named constants
yimsk Jan 14, 2026
e5363ac
fix(header): use max() for availableWidth floor instead of fallback
yimsk Jan 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 29 additions & 10 deletions cmd/claws/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ func main() {
}
cfg.SetReadOnly(opts.readOnly)

var compactHeader bool
if opts.compactHeader != nil {
compactHeader = *opts.compactHeader
} else {
compactHeader = fileCfg.GetCompactHeader()
}
cfg.SetCompactHeader(compactHeader)

for _, p := range opts.profiles {
if !config.IsValidProfileName(p) {
fmt.Fprintf(os.Stderr, "Error: invalid profile name: %s\n", p)
Expand Down Expand Up @@ -116,16 +124,17 @@ func main() {
}

type cliOptions struct {
profiles []string
regions []string
readOnly bool
envCreds bool
autosave *bool
logFile string
configFile string
service string
resourceID string
theme string
profiles []string
regions []string
readOnly bool
envCreds bool
autosave *bool
logFile string
configFile string
service string
resourceID string
theme string
compactHeader *bool
}

// parseFlags parses command line flags and returns options
Expand Down Expand Up @@ -194,6 +203,12 @@ func parseFlagsFromArgs(args []string) cliOptions {
i++
opts.theme = args[i]
}
case "--compact":
t := true
opts.compactHeader = &t
case "--no-compact":
f := false
opts.compactHeader = &f
case "-h", "--help":
showHelp = true
case "-v", "--version":
Expand Down Expand Up @@ -245,6 +260,10 @@ func printUsage() {
fmt.Println(" Enable debug logging to specified file")
fmt.Println(" -t, --theme <name>")
fmt.Println(" Color theme: dark, light, nord, dracula, gruvbox, catppuccin")
fmt.Println(" --compact")
fmt.Println(" Start with compact header mode (toggle with Ctrl+E)")
fmt.Println(" --no-compact")
fmt.Println(" Disable compact header (overrides config file)")
fmt.Println(" -v, --version")
fmt.Println(" Show version")
fmt.Println(" -h, --help")
Expand Down
6 changes: 4 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ cloudwatch:
window: 15m # Metrics data window period (default: 15m)

autosave:
enabled: true # Save region/profile/theme on change (default: false)
enabled: true # Save region/profile/theme/compact_header on change (default: false)

compact_header: false # Use single-line compact header (default: false)

startup: # Applied on launch if present
view: services # Startup view: "dashboard", "services", or "service/resource" (e.g., "ec2", "rds/snapshots")
Expand Down Expand Up @@ -84,7 +86,7 @@ theme: nord # Preset: dark, light, nord, dracula, gruvbox, catppuc

The config file is **not created automatically**. Create it manually if needed.

CLI flags (`-p`, `-r`, `-t`, `--autosave`, `--no-autosave`) override config file settings.
CLI flags (`-p`, `-r`, `-t`, `--compact`, `--no-compact`, `--autosave`, `--no-autosave`) override config file settings.
Multiple values supported: `-p dev,prod` or `-p dev -p prod`.

### Special Profile IDs
Expand Down
2 changes: 2 additions & 0 deletions docs/keybindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Complete reference for all keyboard shortcuts in claws.
| `:services` | Go to service browser |
| `/` | Filter mode (fuzzy search) |
| `A` | AI Chat (Bedrock) |
| `Ctrl+E` | Toggle compact header |
| `?` | Show help |

## Resource Browser
Expand Down Expand Up @@ -63,6 +64,7 @@ Complete reference for all keyboard shortcuts in claws.
| `:diff <n1> <n2>` | Compare two named resources |
| `:theme <name>` | Change color theme |
| `:autosave on/off` | Enable/disable config autosave |
| `:settings` | Show current settings |
| `:clear-history` | Clear navigation history (stack) |

## Mouse Support
Expand Down
48 changes: 36 additions & 12 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,15 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return a, nil

case view.CompactHeaderChangedMsg:
if a.currentView != nil {
a.currentView.Update(msg)
}
for _, v := range a.viewStack {
v.Update(msg)
}
return a, nil

case view.ThemeChangeMsg:
theme := ui.GetPreset(msg.Name)
if theme == nil {
Expand Down Expand Up @@ -340,6 +349,16 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
chatOverlay.Init(),
a.modal.SetSize(a.width, a.height),
)

case key.Matches(msg, a.keys.CompactHeader):
compact := !config.Global().CompactHeader()
config.Global().SetCompactHeader(compact)
if config.File().PersistenceEnabled() {
if err := config.File().SaveCompactHeader(compact); err != nil {
log.Warn("failed to persist compact header", "error", err)
}
}
return a, func() tea.Msg { return view.CompactHeaderChangedMsg{} }
}

case view.ShowModalMsg:
Expand Down Expand Up @@ -504,7 +523,7 @@ func (a *App) View() tea.View {

var statusContent string
if a.commandMode {
statusContent = a.commandInput.View() + " • Esc:cancel Enter:run Tab:complete"
statusContent = a.commandInput.View() + ui.DimStyle().Render(" • Esc:cancel Enter:run Tab:complete")
} else {
if a.err != nil {
statusContent = ui.DangerStyle().Render("Error: " + a.err.Error())
Expand Down Expand Up @@ -774,17 +793,18 @@ func (a *App) refreshCurrentView() (tea.Model, tea.Cmd) {
}

type keyMap struct {
Up key.Binding
Down key.Binding
Enter key.Binding
Back key.Binding
Filter key.Binding
Command key.Binding
Region key.Binding
Profile key.Binding
AI key.Binding
Help key.Binding
Quit key.Binding
Up key.Binding
Down key.Binding
Enter key.Binding
Back key.Binding
Filter key.Binding
Command key.Binding
Region key.Binding
Profile key.Binding
AI key.Binding
CompactHeader key.Binding
Help key.Binding
Quit key.Binding
}

func defaultKeyMap() keyMap {
Expand Down Expand Up @@ -825,6 +845,10 @@ func defaultKeyMap() keyMap {
key.WithKeys("A"),
key.WithHelp("A", "ai chat"),
),
CompactHeader: key.NewBinding(
key.WithKeys("ctrl+e"),
key.WithHelp("ctrl+e", "compact header"),
),
Help: key.NewBinding(
key.WithKeys("?"),
key.WithHelp("?", "help"),
Expand Down
21 changes: 15 additions & 6 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,13 @@ func (s ProfileSelection) ID() string {
}

type Config struct {
mu sync.RWMutex
regions []string
selections []ProfileSelection
accountIDs map[string]string
warnings []string
readOnly bool
mu sync.RWMutex
regions []string
selections []ProfileSelection
accountIDs map[string]string
warnings []string
readOnly bool
compactHeader bool
}

var (
Expand Down Expand Up @@ -346,6 +347,14 @@ func (c *Config) SetReadOnly(readOnly bool) {
doWithLock(&c.mu, func() { c.readOnly = readOnly })
}

func (c *Config) CompactHeader() bool {
return withRLock(&c.mu, func() bool { return c.compactHeader })
}

func (c *Config) SetCompactHeader(compact bool) {
doWithLock(&c.mu, func() { c.compactHeader = compact })
}

func (c *Config) AddWarning(msg string) {
doWithLock(&c.mu, func() { c.warnings = append(c.warnings, msg) })
}
21 changes: 21 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,27 @@ func TestConfig_ReadOnlyGetSet(t *testing.T) {
}
}

func TestConfig_CompactHeaderGetSet(t *testing.T) {
cfg := &Config{}

// Initial value should be false
if cfg.CompactHeader() {
t.Error("CompactHeader() = true, want false")
}

// Set to true
cfg.SetCompactHeader(true)
if !cfg.CompactHeader() {
t.Error("CompactHeader() = false, want true")
}

// Set back to false
cfg.SetCompactHeader(false)
if cfg.CompactHeader() {
t.Error("CompactHeader() = true, want false")
}
}

func TestConfig_Warnings(t *testing.T) {
cfg := &Config{}

Expand Down
18 changes: 18 additions & 0 deletions internal/config/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ type FileConfig struct {
Theme ThemeConfig `yaml:"theme,omitempty"`
Navigation NavigationConfig `yaml:"navigation,omitempty"`
AI AIConfig `yaml:"ai,omitempty"`
CompactHeader bool `yaml:"compact_header,omitempty"`
}

// Duration wraps time.Duration for YAML marshal/unmarshal as string (e.g., "5s", "30s")
Expand Down Expand Up @@ -593,6 +594,23 @@ func (c *FileConfig) SavePersistence(enabled bool) error {
})
}

func (c *FileConfig) GetCompactHeader() bool {
return withRLock(&c.mu, func() bool {
return c.CompactHeader
})
}

func (c *FileConfig) SaveCompactHeader(compact bool) error {
c.mu.Lock()
defer c.mu.Unlock()

c.CompactHeader = compact

return c.patchConfigLocked(func(mapping *yaml.Node) {
setBoolValue(mapping, "compact_header", compact)
})
}

func (c *FileConfig) patchConfigLocked(patchFn func(mapping *yaml.Node)) error {
path, err := ConfigPath()
if err != nil {
Expand Down
15 changes: 15 additions & 0 deletions internal/ui/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"sync"

"charm.land/bubbles/v2/spinner"
"charm.land/bubbles/v2/textinput"
"charm.land/lipgloss/v2"

"github.com/clawscli/claws/internal/config"
Expand Down Expand Up @@ -472,3 +473,17 @@ func NewSpinner() spinner.Model {
s.Style = lipgloss.NewStyle().Foreground(Current().Accent)
return s
}

func TextInputStyles() textinput.Styles {
t := Current()
state := textinput.StyleState{
Text: lipgloss.NewStyle().Foreground(t.Text),
Placeholder: lipgloss.NewStyle().Foreground(t.TextDim),
Suggestion: lipgloss.NewStyle().Foreground(t.TextDim),
Prompt: lipgloss.NewStyle().Foreground(t.Text),
}
return textinput.Styles{
Focused: state,
Blurred: state,
}
}
6 changes: 4 additions & 2 deletions internal/view/command_input.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func newCommandInputStyles() commandInputStyles {
input: ui.InputFieldStyle(),
suggestion: ui.DimStyle(),
highlight: ui.HighlightStyle(),
alias: ui.NoStyle(), // Normal text, not dimmed
alias: ui.TextStyle(),
}
}

Expand Down Expand Up @@ -80,6 +80,7 @@ func NewCommandInput(ctx context.Context, reg *registry.Registry) *CommandInput
ti.Prompt = ":"
ti.CharLimit = 150
ti.SetWidth(commandInputWidth1)
ti.SetStyles(ui.TextInputStyles())

return &CommandInput{
ctx: ctx,
Expand All @@ -101,6 +102,7 @@ func (c *CommandInput) Activate() tea.Cmd {

func (c *CommandInput) ReloadStyles() {
c.styles = newCommandInputStyles()
c.textInput.SetStyles(ui.TextInputStyles())
}

// Deactivate deactivates command mode
Expand Down Expand Up @@ -314,7 +316,7 @@ func (c *CommandInput) View() string {
if len(c.suggestions) > maxShow+1 {
suggText += " ..."
}
suggView = s.alias.Render(suggText) // alias = NoStyle (white)
suggView = s.alias.Render(suggText)
}
}

Expand Down
3 changes: 3 additions & 0 deletions internal/view/dashboard_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ func (d *DashboardView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
d.styles = newDashboardStyles()
d.headerPanel.ReloadStyles()
return d, nil
case CompactHeaderChangedMsg:
d.lastHeaderHeight = 0 // force hitAreas rebuild
return d, nil

case tea.MouseClickMsg:
return d.handleMouseClick(msg)
Expand Down
Loading