Skip to content

Commit c60216e

Browse files
release 0.3.0
Updated dashboard view Updated domains tab with details Fixed tab cycle issue Added route health on dashboard Adaptive layout Tab overflow issue
1 parent c63ea0f commit c60216e

7 files changed

Lines changed: 273 additions & 44 deletions

File tree

internal/tui/components/banner.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,27 @@ import (
1010
// Banner renders a compact single-line app banner: accent bar + name +
1111
// version on the left, status pill flush right. No box, no border.
1212
func Banner(th styles.Theme, version, pill string, width int) string {
13-
if width < 40 {
14-
width = 40
13+
pillW := lipgloss.Width(pill)
14+
if width <= pillW+2 {
15+
return pill
16+
}
17+
left := th.Title.Render("▎ ") + th.Title.Render("mkdev")
18+
tag := versionTag(version)
19+
if width >= lipgloss.Width(left)+len(" · "+tag)+pillW+2 {
20+
left += th.Dim.Render(" · " + tag)
1521
}
16-
left := th.Title.Render("▎ ") + th.Title.Render("mkdev") +
17-
th.Dim.Render(" · v"+version)
1822
leftW := lipgloss.Width(left)
19-
pillW := lipgloss.Width(pill)
2023
gap := max(width-leftW-pillW, 1)
2124
return left + strings.Repeat(" ", gap) + pill
2225
}
26+
27+
// versionTag returns the version with exactly one leading "v".
28+
func versionTag(version string) string {
29+
if version == "" {
30+
return "v?"
31+
}
32+
if version[0] == 'v' || version[0] == 'V' {
33+
return version
34+
}
35+
return "v" + version
36+
}

internal/tui/components/components_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111

1212
func TestTabBarHighlightsActive(t *testing.T) {
1313
th := styles.NewTheme()
14-
out := components.TabBar(th, []string{"Domains", "Projects", "Logs"}, 1)
14+
out := components.TabBar(th, []string{"Domains", "Projects", "Logs"}, 1, 120)
1515
require.Contains(t, out, "Domains")
1616
require.Contains(t, out, "Projects")
1717
require.Contains(t, out, "Logs")

internal/tui/components/tabbar.go

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strings"
66

77
"github.com/charmbracelet/lipgloss"
8+
89
"github.com/venkatkrishna07/mkdev/internal/tui/styles"
910
)
1011

@@ -19,31 +20,73 @@ func useGlyphs() bool {
1920
return os.Getenv("NO_NERD_FONT") == "" && os.Getenv("NO_COLOR") == ""
2021
}
2122

22-
// TabBar renders a single-line tab strip. Active uses TabActive (filled
23-
// background); inactives use TabInactive.
24-
func TabBar(th styles.Theme, labels []string, active int) string {
23+
// TabBar renders a single-line tab strip. width caps the output; the bar
24+
// degrades to icon-only labels and finally to the active tab alone with
25+
// arrow hints when the full bar would overflow.
26+
func TabBar(th styles.Theme, labels []string, active, width int) string {
2527
tabs := make([]Tab, len(labels))
2628
for i, l := range labels {
2729
tabs[i] = Tab{Label: l}
2830
}
29-
return TabBarRich(th, tabs, active)
31+
return TabBarRich(th, tabs, active, width)
3032
}
3133

32-
// TabBarRich draws tabs with optional per-tab icons.
33-
func TabBarRich(th styles.Theme, tabs []Tab, active int) string {
34-
parts := make([]string, len(tabs))
34+
// TabBarRich draws tabs with optional per-tab icons, falling back to a
35+
// compact form when the rendered width exceeds the available terminal width.
36+
func TabBarRich(th styles.Theme, tabs []Tab, active, width int) string {
3537
glyphs := useGlyphs()
38+
sep := lipgloss.NewStyle().Foreground(th.Muted).Render("│")
39+
40+
build := func(labels []string) string {
41+
parts := make([]string, len(labels))
42+
for i, label := range labels {
43+
if i == active {
44+
parts[i] = th.TabActive.Render(label)
45+
} else {
46+
parts[i] = th.TabInactive.Render(label)
47+
}
48+
}
49+
return strings.Join(parts, sep)
50+
}
51+
52+
full := make([]string, len(tabs))
3653
for i, t := range tabs {
3754
label := t.Label
3855
if glyphs && t.Icon != "" {
3956
label = t.Icon + " " + t.Label
4057
}
41-
if i == active {
42-
parts[i] = th.TabActive.Render(label)
43-
} else {
44-
parts[i] = th.TabInactive.Render(label)
58+
full[i] = label
59+
}
60+
rendered := build(full)
61+
if width <= 0 || lipgloss.Width(rendered) <= width {
62+
return rendered
63+
}
64+
65+
if glyphs {
66+
iconsOnly := make([]string, len(tabs))
67+
anyIcon := false
68+
for i, t := range tabs {
69+
if t.Icon != "" {
70+
iconsOnly[i] = t.Icon
71+
anyIcon = true
72+
} else {
73+
iconsOnly[i] = t.Label
74+
}
75+
}
76+
if anyIcon {
77+
rendered = build(iconsOnly)
78+
if lipgloss.Width(rendered) <= width {
79+
return rendered
80+
}
4581
}
4682
}
47-
sep := lipgloss.NewStyle().Foreground(th.Muted).Render("│")
48-
return strings.Join(parts, sep)
83+
84+
activeLabel := full[active]
85+
prev := th.Dim.Render("‹")
86+
next := th.Dim.Render("›")
87+
compact := prev + " " + th.TabActive.Render(activeLabel) + " " + next
88+
if lipgloss.Width(compact) <= width {
89+
return compact
90+
}
91+
return th.TabActive.Render(activeLabel)
4992
}

internal/tui/modal_dispatch.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ func (m rootModel) updateTopModal(msg tea.Msg) (tea.Model, tea.Cmd) {
3434
case modals.Confirm:
3535
t, cmd = t.Update(msg)
3636
m.modals[idx] = t
37+
case modals.Help:
38+
t, cmd = t.Update(msg)
39+
m.modals[idx] = t
3740
}
3841
return m, cmd
3942
}
@@ -72,6 +75,8 @@ func (m rootModel) activeKeyMap() help.KeyMap {
7275
return t.Keys()
7376
case modals.Confirm:
7477
return t.Keys()
78+
case modals.Help:
79+
return t.Keys()
7580
}
7681
return m.keys
7782
}

internal/tui/modals/help.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package modals
2+
3+
import (
4+
"strings"
5+
6+
"github.com/charmbracelet/bubbles/help"
7+
"github.com/charmbracelet/bubbles/key"
8+
tea "github.com/charmbracelet/bubbletea"
9+
"github.com/venkatkrishna07/mkdev/internal/tui/styles"
10+
)
11+
12+
// HelpKeys are the keybindings advertised by the Help modal.
13+
type HelpKeys struct {
14+
Close key.Binding
15+
}
16+
17+
// ShortHelp implements help.KeyMap.
18+
func (k HelpKeys) ShortHelp() []key.Binding { return []key.Binding{k.Close} }
19+
20+
// FullHelp implements help.KeyMap.
21+
func (k HelpKeys) FullHelp() [][]key.Binding { return [][]key.Binding{{k.Close}} }
22+
23+
// DefaultHelpKeys is the default Help-modal binding set.
24+
var DefaultHelpKeys = HelpKeys{
25+
Close: key.NewBinding(key.WithKeys("esc", "enter", "?"), key.WithHelp("esc/↵/?", "close")),
26+
}
27+
28+
// Help is a read-only modal that lists key bindings.
29+
type Help struct {
30+
th styles.Theme
31+
bindings []key.Binding
32+
}
33+
34+
// NewHelp constructs a Help modal listing bindings.
35+
func NewHelp(th styles.Theme, bindings []key.Binding) Help {
36+
return Help{th: th, bindings: bindings}
37+
}
38+
39+
// Title implements Modal.
40+
func (h Help) Title() string { return "Help" }
41+
42+
// Keys returns the modal's help.KeyMap.
43+
func (h Help) Keys() help.KeyMap { return DefaultHelpKeys }
44+
45+
// Init implements tea.Model.
46+
func (h Help) Init() tea.Cmd { return nil }
47+
48+
// Update advances the modal in response to a tea.Msg.
49+
func (h Help) Update(msg tea.Msg) (Help, tea.Cmd) {
50+
if k, ok := msg.(tea.KeyMsg); ok {
51+
switch k.Type {
52+
case tea.KeyEsc, tea.KeyEnter:
53+
return h, func() tea.Msg { return Closed{Result: Result{Cancelled: true}} }
54+
}
55+
if k.String() == "?" {
56+
return h, func() tea.Msg { return Closed{Result: Result{Cancelled: true}} }
57+
}
58+
}
59+
return h, nil
60+
}
61+
62+
// View renders the Help modal.
63+
func (h Help) View() string {
64+
keyCol := 0
65+
for _, b := range h.bindings {
66+
if w := len(b.Help().Key); w > keyCol {
67+
keyCol = w
68+
}
69+
}
70+
if keyCol < 6 {
71+
keyCol = 6
72+
}
73+
74+
var b strings.Builder
75+
b.WriteString(h.th.ModalTitle.Render("Key bindings"))
76+
b.WriteString("\n\n")
77+
for _, kb := range h.bindings {
78+
hk := kb.Help()
79+
if hk.Key == "" && hk.Desc == "" {
80+
continue
81+
}
82+
pad := max(keyCol-len(hk.Key), 0)
83+
b.WriteString(h.th.FooterKey.Render(hk.Key))
84+
b.WriteString(strings.Repeat(" ", pad+3))
85+
b.WriteString(h.th.Footer.Render(hk.Desc))
86+
b.WriteString("\n")
87+
}
88+
b.WriteString("\n")
89+
b.WriteString(h.th.Dim.Render("esc/↵/? close"))
90+
return h.th.Modal.Width(50).Render(b.String())
91+
}

internal/tui/program.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,12 @@ type rootModel struct {
7171
logs tabs.Logs
7272
doctor tabs.Doctor
7373
settings tabs.Settings
74-
modals []any // LIFO stack of modals.Add / modals.Edit / modals.Confirm
74+
modals []any // LIFO stack of modals.Add / modals.Edit / modals.Confirm / modals.Help
7575
proxy ProxyState
7676
proxyCh <-chan ProxyState
7777
binPath string
7878
keys KeyMap
7979
help help.Model
80-
showHelp bool
8180
spinner spinner.Model
8281
busy bool
8382
active tabIndex
@@ -287,7 +286,11 @@ func (m rootModel) handleGlobalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
287286
m.pendingQuit = true
288287
return m, nil
289288
case key.Matches(k, m.keys.Help):
290-
m.showHelp = !m.showHelp
289+
var flat []key.Binding
290+
for _, row := range m.keys.FullHelp() {
291+
flat = append(flat, row...)
292+
}
293+
m.modals = append(m.modals, modals.NewHelp(m.th, flat))
291294
return m, nil
292295
case key.Matches(k, m.keys.NextTab):
293296
m.active = (m.active + 1) % tabIndex(len(tabLabels))
@@ -388,7 +391,7 @@ func (m rootModel) View() string {
388391
}
389392

390393
header := components.Banner(m.th, version.Version, pill, width)
391-
tabBar := components.TabBarRich(m.th, tabSpecs, int(m.active))
394+
tabBar := components.TabBarRich(m.th, tabSpecs, int(m.active), width)
392395
rule := m.th.Rule.Render(strings.Repeat("─", width))
393396
var body string
394397
switch m.active {
@@ -409,7 +412,6 @@ func (m rootModel) View() string {
409412
body = m.spinner.View() + " " + m.th.Dim.Render("working…") + "\n" + body
410413
}
411414

412-
m.help.ShowAll = m.showHelp
413415
footer := m.help.View(m.activeKeyMap())
414416

415417
var toast string
@@ -423,6 +425,9 @@ func (m rootModel) View() string {
423425
}
424426
sections = append(sections, footer)
425427
view := lipgloss.JoinVertical(lipgloss.Left, sections...)
428+
if m.height > 0 {
429+
view = lipgloss.Place(m.width, m.height, lipgloss.Left, lipgloss.Top, view)
430+
}
426431

427432
if len(m.modals) == 0 {
428433
return view
@@ -436,6 +441,8 @@ func (m rootModel) View() string {
436441
modalView = t.View()
437442
case modals.Confirm:
438443
modalView = t.View()
444+
case modals.Help:
445+
modalView = t.View()
439446
}
440447
return lipgloss.Place(
441448
m.width, m.height,

0 commit comments

Comments
 (0)