From a65c98cc5ef5f798503799bcc60eb3a800449e72 Mon Sep 17 00:00:00 2001 From: stawan15 Date: Sun, 19 Jul 2026 12:45:53 +0700 Subject: [PATCH 1/2] feat: add header bar with project name and git branch, and update log panel to display action-specific titles --- main.go | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++----- styles.go | 32 ++++++++++++++------ 2 files changed, 103 insertions(+), 16 deletions(-) diff --git a/main.go b/main.go index 765df3e..2d85dbf 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "os" + "os/exec" + "path/filepath" "strings" "github.com/charmbracelet/bubbles/list" @@ -93,6 +95,41 @@ type model struct { confirmAct actionItem confirmDest string confirmVer string + + // Header info + projectName string + gitBranch string +} + +// detectProjectName tries to get a short project name from the git remote URL +// or falls back to the current directory name. +func detectProjectName() string { + out, err := exec.Command("git", "remote", "get-url", "origin").Output() + if err == nil { + remote := strings.TrimSpace(string(out)) + // strip .git suffix and take last path component + remote = strings.TrimSuffix(remote, ".git") + parts := strings.FieldsFunc(remote, func(r rune) bool { + return r == '/' || r == ':' + }) + if len(parts) > 0 { + return parts[len(parts)-1] + } + } + // fallback: current directory name + if cwd, err := os.Getwd(); err == nil { + return filepath.Base(cwd) + } + return "kamal-tui" +} + +// detectGitBranch returns the current git branch name. +func detectGitBranch() string { + out, err := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output() + if err != nil { + return "" + } + return strings.TrimSpace(string(out)) } func initialModel() model { @@ -101,7 +138,7 @@ func initialModel() model { items = append(items, a) } al := list.New(items, list.NewDefaultDelegate(), 0, 0) - al.Title = "Actions" + al.Title = "Menu" al.SetShowStatusBar(false) al.SetFilteringEnabled(false) al.SetShowHelp(false) @@ -158,6 +195,8 @@ func initialModel() model { secList: secList, secKeyIn: secKeyIn, secValIn: secValIn, + projectName: detectProjectName(), + gitBranch: detectGitBranch(), } } @@ -183,8 +222,9 @@ func waitForDone(ch <-chan error) tea.Cmd { } func (m *model) layout() { + headerH := 1 footerH := 1 - bodyH := m.height - footerH + bodyH := m.height - headerH - footerH if bodyH < 3 { bodyH = 3 } @@ -517,6 +557,7 @@ func (m model) startRun(action actionItem, dest, version string) (tea.Model, tea m.lineCh = make(chan string) m.doneCh = make(chan error, 1) m.running = true + m.selectedAction = action m.outputBuf = nil m.statusLine = "" m.lastErr = nil @@ -533,6 +574,26 @@ func (m model) startRun(action actionItem, dest, version string) (tea.Model, tea return m, tea.Batch(m.spinner.Tick, waitForLine(m.lineCh), waitForDone(m.doneCh)) } +// headerView renders the top bar: empty left side + project::branch right-aligned. +func (m model) headerView() string { + var label string + if m.gitBranch != "" { + label = m.projectName + " :: " + m.gitBranch + } else { + label = m.projectName + } + right := headerBranchStyle.Render(label) + // Pad left so right label is flush right + rightW := lipgloss.Width(right) + padding := m.width - rightW + if padding < 0 { + padding = 0 + } + return lipgloss.NewStyle().Background(colorHeaderBg).Width(m.width).Render( + strings.Repeat(" ", padding) + right, + ) +} + func (m model) View() string { if m.width == 0 { return "loading…" @@ -578,8 +639,9 @@ func (m model) View() string { } rightW := m.width - leftW + headerH := 1 footerH := 1 - bodyH := m.height - footerH + bodyH := m.height - headerH - footerH destH := bodyH / 2 actionH := bodyH - destH @@ -593,19 +655,28 @@ func (m model) View() string { } destPanel = style.Width(leftW - 2).Height(destH - 2).Render(m.destList.View()) - // Render Actions + // Render Menu (Actions) style = inactivePanelStyle if m.activePanel == panelActions { style = activePanelStyle } actionPanel = style.Width(leftW - 2).Height(actionH - 2).Render(m.actionList.View()) - // Render Logs + // Render Logs panel with dynamic title style = inactivePanelStyle if m.activePanel == panelLogs { style = activePanelStyle } + // Build log panel title: "{ActionName} logs" or just "logs" + logTitle := "logs" + if m.selectedAction.title != "" { + // Strip emoji from title for cleanliness + clean := strings.TrimSpace(m.selectedAction.title) + logTitle = clean + " logs" + } + logPanelTitle := logPanelTitleStyle.Render(logTitle) + logContent := m.viewport.View() if m.showVersionInput { overlay := lipgloss.JoinVertical(lipgloss.Left, @@ -616,12 +687,14 @@ func (m model) View() string { logContent = lipgloss.Place(rightW-4, bodyH-4, lipgloss.Center, lipgloss.Center, activePanelStyle.Render(overlay)) } - logPanel = style.Width(rightW - 2).Height(bodyH - 2).Render(logContent) + // Compose log panel: title on top, viewport below, inside the border style + logInner := lipgloss.JoinVertical(lipgloss.Left, logPanelTitle, logContent) + logPanel = style.Width(rightW - 2).Height(bodyH - 2).Render(logInner) leftCol := lipgloss.JoinVertical(lipgloss.Left, destPanel, actionPanel) mainView := lipgloss.JoinHorizontal(lipgloss.Top, leftCol, logPanel) - return lipgloss.JoinVertical(lipgloss.Left, mainView, m.footerView()) + return lipgloss.JoinVertical(lipgloss.Left, m.headerView(), mainView, m.footerView()) } func destLabel(d string) string { diff --git a/styles.go b/styles.go index bf36722..506d8ea 100644 --- a/styles.go +++ b/styles.go @@ -4,15 +4,16 @@ import "github.com/charmbracelet/lipgloss" var ( // Tokyo Night-ish / Modern theme - colorBg = lipgloss.Color("#1a1b26") - colorFg = lipgloss.Color("#c0caf5") - colorAccent = lipgloss.Color("#7aa2f7") // Blue - colorActive = lipgloss.Color("#bb9af7") // Purple - colorBorder = lipgloss.Color("#414868") // Dark Gray - colorMuted = lipgloss.Color("#565f89") - colorGood = lipgloss.Color("#9ece6a") // Green - colorBad = lipgloss.Color("#f7768e") // Red - colorWarning = lipgloss.Color("#e0af68") // Yellow + colorBg = lipgloss.Color("#1a1b26") + colorFg = lipgloss.Color("#c0caf5") + colorAccent = lipgloss.Color("#7aa2f7") // Blue + colorActive = lipgloss.Color("#bb9af7") // Purple + colorBorder = lipgloss.Color("#414868") // Dark Gray + colorMuted = lipgloss.Color("#565f89") + colorGood = lipgloss.Color("#9ece6a") // Green + colorBad = lipgloss.Color("#f7768e") // Red + colorWarning = lipgloss.Color("#e0af68") // Yellow + colorHeaderBg = lipgloss.Color("#16161e") // Slightly darker for header strip titleStyle = lipgloss.NewStyle(). Bold(true). @@ -46,4 +47,17 @@ var ( badStyle = lipgloss.NewStyle().Foreground(colorBad).Bold(true) spinnerStyle = lipgloss.NewStyle().Foreground(colorAccent) + + // Header bar: project :: branch shown top-right + headerBranchStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(colorAccent). + Background(colorHeaderBg). + Padding(0, 1) + + // Log panel inner title: "{Action} logs" + logPanelTitleStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(colorMuted). + Padding(0, 0, 0, 1) ) From b92718cf5179847a0c79d853ffccffb7f24a1e10 Mon Sep 17 00:00:00 2001 From: stawan15 Date: Sun, 19 Jul 2026 12:50:21 +0700 Subject: [PATCH 2/2] style: update action menu titles with Nerd Font icons --- kamal.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/kamal.go b/kamal.go index 5f73def..c5cd73b 100644 --- a/kamal.go +++ b/kamal.go @@ -29,35 +29,35 @@ func (a actionItem) FilterValue() string { return a.title } func actions() []actionItem { return []actionItem{ { - title: "πŸš€ Deploy", + title: "󰚰 Deploy", desc: "kamal deploy -d ", buildArgs: func(dest, _ string) []string { return withDest([]string{"deploy"}, dest) }, }, { - title: "βš™οΈ Setup", + title: "σ°’“ Setup", desc: "kamal setup -d (provision servers & deploy)", buildArgs: func(dest, _ string) []string { return withDest([]string{"setup"}, dest) }, }, { - title: "πŸ”‘ Env Push", + title: "σ°ˆ™ Env Push", desc: "kamal env push -d (push .env variables to servers)", buildArgs: func(dest, _ string) []string { return withDest([]string{"env", "push"}, dest) }, }, { - title: "♻️ Redeploy", + title: "σ°‘™ Redeploy", desc: "kamal redeploy -d (skip build cache invalidation steps)", buildArgs: func(dest, _ string) []string { return withDest([]string{"redeploy"}, dest) }, }, { - title: "βͺ Rollback", + title: "󰁯 Rollback", desc: "kamal rollback -d ", needsVersion: true, buildArgs: func(dest, version string) []string { @@ -66,7 +66,7 @@ func actions() []actionItem { }, }, { - title: "πŸ’Ύ DB Dump (Backup)", + title: "σ°†Ό DB Dump (Backup)", desc: "kamal app exec -i -- /bin/sh -c 'pg_dump ...'", buildArgs: func(dest, _ string) []string { args := []string{"app", "exec", "-i"} @@ -76,7 +76,7 @@ func actions() []actionItem { }, }, { - title: "πŸ’Ώ DB Restore", + title: "σ°—¨ DB Restore", desc: "kamal app exec -i -- /bin/sh -c 'pg_restore ...'", buildArgs: func(dest, _ string) []string { args := []string{"app", "exec", "-i"} @@ -86,35 +86,35 @@ func actions() []actionItem { }, }, { - title: "ℹ️ App Details", + title: "σ°‹© App Details", desc: "kamal app details -d ", buildArgs: func(dest, _ string) []string { return withDest([]string{"app", "details"}, dest) }, }, { - title: "πŸ“ App Logs", + title: "σ°…© App Logs", desc: "kamal app logs -d (last lines, no follow)", buildArgs: func(dest, _ string) []string { return withDest([]string{"app", "logs"}, dest) }, }, { - title: "⚑ App Boot", + title: "󰀡 App Boot", desc: "kamal app boot -d ", buildArgs: func(dest, _ string) []string { return withDest([]string{"app", "boot"}, dest) }, }, { - title: "πŸ•’ Audit", + title: "󰚌 Audit", desc: "kamal audit -d (recent deploy history)", buildArgs: func(dest, _ string) []string { return withDest([]string{"audit"}, dest) }, }, { - title: "πŸ—‘οΈ Remove", + title: "󰆴 Remove", desc: "kamal remove -d (remove containers and images from servers)", buildArgs: func(dest, _ string) []string { return withDest([]string{"remove"}, dest)