Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 12 additions & 12 deletions kamal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <destination>",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"deploy"}, dest)
},
},
{
title: "⚙️ Setup",
title: "󰒓 Setup",
desc: "kamal setup -d <destination> (provision servers & deploy)",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"setup"}, dest)
},
},
{
title: "🔑 Env Push",
title: "󰈙 Env Push",
desc: "kamal env push -d <destination> (push .env variables to servers)",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"env", "push"}, dest)
},
},
{
title: "♻️ Redeploy",
title: "󰑙 Redeploy",
desc: "kamal redeploy -d <destination> (skip build cache invalidation steps)",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"redeploy"}, dest)
},
},
{
title: " Rollback",
title: "󰁯 Rollback",
desc: "kamal rollback <version> -d <destination>",
needsVersion: true,
buildArgs: func(dest, version string) []string {
Expand All @@ -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"}
Expand All @@ -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"}
Expand All @@ -86,35 +86,35 @@ func actions() []actionItem {
},
},
{
title: "ℹ️ App Details",
title: "󰋩 App Details",
desc: "kamal app details -d <destination>",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"app", "details"}, dest)
},
},
{
title: "📝 App Logs",
title: "󰅩 App Logs",
desc: "kamal app logs -d <destination> (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 <destination>",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"app", "boot"}, dest)
},
},
{
title: "🕒 Audit",
title: "󰚌 Audit",
desc: "kamal audit -d <destination> (recent deploy history)",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"audit"}, dest)
},
},
{
title: "🗑️ Remove",
title: "󰆴 Remove",
desc: "kamal remove -d <destination> (remove containers and images from servers)",
buildArgs: func(dest, _ string) []string {
return withDest([]string{"remove"}, dest)
Expand Down
87 changes: 80 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/charmbracelet/bubbles/list"
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -158,6 +195,8 @@ func initialModel() model {
secList: secList,
secKeyIn: secKeyIn,
secValIn: secValIn,
projectName: detectProjectName(),
gitBranch: detectGitBranch(),
}
}

Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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…"
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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 {
Expand Down
32 changes: 23 additions & 9 deletions styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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)
)
Loading