From 711bcc01fd11fa0ce2a1c3e99941d43d939912e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20L=C3=B3pez?= Date: Mon, 13 Jul 2026 22:04:12 +0200 Subject: [PATCH 1/2] dashboard: centre everything, with the workspace list as a left-aligned block Per-line centring left the workspace rows ragged. Centre the standalone lines (banner, dividers, summary, footer) individually as before, but centre the workspace list as a single block (blockCenter) so its name/data columns stay left-aligned while the block sits in the middle. --- internal/tui/tui.go | 79 ++++++++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/internal/tui/tui.go b/internal/tui/tui.go index d22b588..a69cf8a 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -387,52 +387,46 @@ func (m model) handleKey(key tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } -// frame places the centred banner above left-aligned content, so the identity -// stays centred while the working area reads like a normal CLI. -func (m model) frame(content string) string { - banner := Banner() - if m.width > 0 { - banner = lipgloss.PlaceHorizontal(m.width, lipgloss.Center, banner) - } - return banner + "\n\n" + content -} - func (m model) View() string { + // Everything is centred on the terminal. Standalone lines (banner, dividers, + // summary, footer) centre individually; the workspace list centres as one + // block so its columns stay left-aligned. var b strings.Builder - b.WriteString(divider()) + b.WriteString(center(m.width, Banner())) + b.WriteString("\n\n") + b.WriteString(center(m.width, divider())) b.WriteString("\n\n") if len(m.items) == 0 { - b.WriteString("No workspaces yet.\n\n") - b.WriteString(metaStyle.Render("Run crabby init inside a project,\n")) - b.WriteString(metaStyle.Render("or press n to add the current directory.\n\n")) - b.WriteString(divider()) - b.WriteString("\n") - b.WriteString(actionKey("n", "new") + " " + actionKey("q", "quit")) - return m.frame(b.String()) + b.WriteString(center(m.width, "No workspaces yet.") + "\n\n") + b.WriteString(center(m.width, metaStyle.Render("Run crabby init inside a project,")) + "\n") + b.WriteString(center(m.width, metaStyle.Render("or press n to add the current directory.")) + "\n\n") + b.WriteString(center(m.width, divider()) + "\n") + b.WriteString(center(m.width, actionKey("n", "new")+" "+actionKey("q", "quit"))) + return b.String() } - b.WriteString(m.renderRows()) + b.WriteString(blockCenter(m.width, m.renderRows())) b.WriteString("\n") - b.WriteString(divider()) + b.WriteString(center(m.width, divider())) b.WriteString("\n") if summary := m.summaryLine(); summary != "" { - b.WriteString(summary + "\n\n") + b.WriteString(center(m.width, summary) + "\n\n") } switch m.confirm { case confirmStop: - b.WriteString(confirmStyle.Render(fmt.Sprintf("Stop \"%s\"? (y/n)", m.items[m.cursor].displayName()))) - return m.frame(b.String()) + b.WriteString(center(m.width, confirmStyle.Render(fmt.Sprintf("Stop \"%s\"? (y/n)", m.items[m.cursor].displayName())))) + return b.String() case confirmRemoveTask: it := m.items[m.cursor] - b.WriteString(confirmStyle.Render(fmt.Sprintf("Remove task \"%s\" from %q? (y/n)", it.task.Name, it.workspace.Name))) - return m.frame(b.String()) + b.WriteString(center(m.width, confirmStyle.Render(fmt.Sprintf("Remove task \"%s\" from %q? (y/n)", it.task.Name, it.workspace.Name)))) + return b.String() case confirmRemoveWorkspace: - b.WriteString(confirmStyle.Render(fmt.Sprintf("Remove workspace \"%s\" from Crabby? Files are kept. (y/n)", m.items[m.cursor].workspace.Name))) - return m.frame(b.String()) + b.WriteString(center(m.width, confirmStyle.Render(fmt.Sprintf("Remove workspace \"%s\" from Crabby? Files are kept. (y/n)", m.items[m.cursor].workspace.Name)))) + return b.String() } - b.WriteString(m.helpLines()) - return m.frame(b.String()) + b.WriteString(center(m.width, m.helpLines())) + return b.String() } // renderRows lays every workspace/task line out in two columns: the name on the @@ -465,6 +459,33 @@ func (m model) renderRows() string { return b.String() } +// blockCenter centres a multi-line block as a unit: every line gets the same +// left margin, so the block sits in the middle while its internal left-aligned +// columns stay aligned (unlike per-line centring, which lets each line drift). +func blockCenter(width int, s string) string { + if width <= 0 { + return s + } + lines := strings.Split(s, "\n") + blockWidth := 0 + for _, l := range lines { + if w := lipgloss.Width(l); w > blockWidth { + blockWidth = w + } + } + margin := (width - blockWidth) / 2 + if margin <= 0 { + return s + } + pad := strings.Repeat(" ", margin) + for i, l := range lines { + if l != "" { + lines[i] = pad + l + } + } + return strings.Join(lines, "\n") +} + // helpLines renders the footer with action keys highlighted so they read as // controls, distinct from the informational summary above them. func (m model) helpLines() string { From a4df8705efb2b640e104815408afa9ad986cd4ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20L=C3=B3pez?= Date: Mon, 13 Jul 2026 22:05:43 +0200 Subject: [PATCH 2/2] v0.7.1: workspace list centred as a left-aligned block --- CHANGELOG.md | 9 +++++++++ VERSION | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6114095..0613a02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.7.1] - 2026-07-13 + +### Changed + +- Dashboard layout: everything is centred again, but the workspace list is + centred as a single block so its name and data columns stay left-aligned + (instead of each line drifting to its own centre). The banner, dividers, usage + summary, and footer keys remain centred line by line. + ## [0.7.0] - 2026-07-13 ### Added diff --git a/VERSION b/VERSION index faef31a..39e898a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.0 +0.7.1