on main help sign in
feat(ui): restructure UI package into subpackages
commit8783795bcbeedfafd2b58b566a5589d8d6bb8f6f authorAyman Bagabas <ayman.bagabas@gmail.com> dateTue 28 Oct 2025, 15:40 (1 year ago) parent706e182
8 files changed+70−47
internal/cmd/root.go+4−2 internal/ui/common.go → internal/ui/common/common.go+7−4 internal/ui/dialog.go → internal/ui/dialog/dialog.go+12−11 internal/ui/quit_dialog.go → internal/ui/dialog/quit.go+13−12 internal/ui/model.go−9 internal/ui/keys.go → internal/ui/model/keys.go+1−1 internal/ui/ui.go → internal/ui/model/ui.go+32−7 internal/ui/styles.go → internal/ui/styles/styles.go+1−1
internal/cmd/root.go+4−2
@@ -18,7 +18,8 @@ import (
1818 "github.com/charmbracelet/crush/internal/db"
1919 "github.com/charmbracelet/crush/internal/event"
2020 "github.com/charmbracelet/crush/internal/tui"
21− "github.com/charmbracelet/crush/internal/ui"
21+ "github.com/charmbracelet/crush/internal/ui/common"
22+ ui "github.com/charmbracelet/crush/internal/ui/model"
2223 "github.com/charmbracelet/crush/internal/version"
2324 "github.com/charmbracelet/fang"
2425 "github.com/charmbracelet/lipgloss/v2"
@@ -83,7 +84,8 @@ crush -y
8384
8485 // Set up the TUI.
8586 // ui := tui.New(app)
86− ui := ui.New(app)
87+ com := common.DefaultCommon(app.Config())
88+ ui := ui.New(com, app)
8789 program := tea.NewProgram(
8890 ui,
8991 tea.WithContext(cmd.Context()),
internal/ui/common.go → internal/ui/common/common.gorenamed+7−4
@@ -1,17 +1,20 @@
1−package ui
1+package common
22
3−import "github.com/charmbracelet/crush/internal/config"
3+import (
4+ "github.com/charmbracelet/crush/internal/config"
5+ "github.com/charmbracelet/crush/internal/ui/styles"
6+)
47
58// Common defines common UI options and configurations.
69type Common struct {
710 Config *config.Config
8− Styles Styles
11+ Styles styles.Styles
912}
1013
1114// DefaultCommon returns the default common UI configurations.
1215func DefaultCommon(cfg *config.Config) *Common {
1316 return &Common{
1417 Config: cfg,
15− Styles: DefaultStyles(),
18+ Styles: styles.DefaultStyles(),
1619 }
1720}
internal/ui/dialog.go → internal/ui/dialog/dialog.gorenamed+12−11
@@ -1,19 +1,20 @@
1−package ui
1+package dialog
22
33import (
44 "github.com/charmbracelet/bubbles/v2/key"
55 tea "github.com/charmbracelet/bubbletea/v2"
6+ "github.com/charmbracelet/crush/internal/ui/component"
67 "github.com/charmbracelet/lipgloss/v2"
78)
89
9−// DialogOverlayKeyMap defines key bindings for dialogs.
10−type DialogOverlayKeyMap struct {
10+// OverlayKeyMap defines key bindings for dialogs.
11+type OverlayKeyMap struct {
1112 Close key.Binding
1213}
1314
14−// DefaultDialogOverlayKeyMap returns the default key bindings for dialogs.
15−func DefaultDialogOverlayKeyMap() DialogOverlayKeyMap {
16− return DialogOverlayKeyMap{
15+// DefaultOverlayKeyMap returns the default key bindings for dialogs.
16+func DefaultOverlayKeyMap() OverlayKeyMap {
17+ return OverlayKeyMap{
1718 Close: key.NewBinding(
1819 key.WithKeys("esc", "alt+esc"),
1920 ),
@@ -22,21 +23,21 @@ func DefaultDialogOverlayKeyMap() DialogOverlayKeyMap {
2223
2324// Dialog is a component that can be displayed on top of the UI.
2425type Dialog interface {
25− Model[Dialog]
26+ component.Model[Dialog]
2627 ID() string
2728}
2829
2930// Overlay manages multiple dialogs as an overlay.
3031type Overlay struct {
3132 dialogs []Dialog
32− keyMap DialogOverlayKeyMap
33+ keyMap OverlayKeyMap
3334}
3435
35−// NewDialogOverlay creates a new [Overlay] instance.
36−func NewDialogOverlay(dialogs ...Dialog) *Overlay {
36+// NewOverlay creates a new [Overlay] instance.
37+func NewOverlay(dialogs ...Dialog) *Overlay {
3738 return &Overlay{
3839 dialogs: dialogs,
39− keyMap: DefaultDialogOverlayKeyMap(),
40+ keyMap: DefaultOverlayKeyMap(),
4041 }
4142}
4243
internal/ui/quit_dialog.go → internal/ui/dialog/quit.gorenamed+13−12
@@ -1,8 +1,9 @@
1−package ui
1+package dialog
22
33import (
44 "github.com/charmbracelet/bubbles/v2/key"
55 tea "github.com/charmbracelet/bubbletea/v2"
6+ "github.com/charmbracelet/crush/internal/ui/common"
67 "github.com/charmbracelet/lipgloss/v2"
78)
89
@@ -46,16 +47,16 @@ func DefaultQuitKeyMap() QuitDialogKeyMap {
4647 }
4748}
4849
49−// QuitDialog represents a confirmation dialog for quitting the application.
50−type QuitDialog struct {
51− com *Common
50+// Quit represents a confirmation dialog for quitting the application.
51+type Quit struct {
52+ com *common.Common
5253 keyMap QuitDialogKeyMap
5354 selectedNo bool // true if "No" button is selected
5455}
5556
56−// NewQuitDialog creates a new quit confirmation dialog.
57−func NewQuitDialog(com *Common) *QuitDialog {
58− q := &QuitDialog{
57+// NewQuit creates a new quit confirmation dialog.
58+func NewQuit(com *common.Common) *Quit {
59+ q := &Quit{
5960 com: com,
6061 keyMap: DefaultQuitKeyMap(),
6162 }
@@ -63,12 +64,12 @@ func NewQuitDialog(com *Common) *QuitDialog {
6364}
6465
6566// ID implements [Model].
66−func (*QuitDialog) ID() string {
67+func (*Quit) ID() string {
6768 return "quit"
6869}
6970
7071// Update implements [Model].
71−func (q *QuitDialog) Update(msg tea.Msg) (Dialog, tea.Cmd) {
72+func (q *Quit) Update(msg tea.Msg) (Dialog, tea.Cmd) {
7273 switch msg := msg.(type) {
7374 case tea.KeyPressMsg:
7475 switch {
@@ -91,7 +92,7 @@ func (q *QuitDialog) Update(msg tea.Msg) (Dialog, tea.Cmd) {
9192}
9293
9394// View implements [Model].
94−func (q *QuitDialog) View() string {
95+func (q *Quit) View() string {
9596 const question = "Are you sure you want to quit?"
9697 baseStyle := q.com.Styles.Base
9798 yesStyle := q.com.Styles.ButtonSelected
@@ -125,7 +126,7 @@ func (q *QuitDialog) View() string {
125126}
126127
127128// ShortHelp implements [help.KeyMap].
128−func (q *QuitDialog) ShortHelp() []key.Binding {
129+func (q *Quit) ShortHelp() []key.Binding {
129130 return []key.Binding{
130131 q.keyMap.LeftRight,
131132 q.keyMap.EnterSpace,
@@ -133,7 +134,7 @@ func (q *QuitDialog) ShortHelp() []key.Binding {
133134}
134135
135136// FullHelp implements [help.KeyMap].
136−func (q *QuitDialog) FullHelp() [][]key.Binding {
137+func (q *Quit) FullHelp() [][]key.Binding {
137138 return [][]key.Binding{
138139 {q.keyMap.LeftRight, q.keyMap.EnterSpace, q.keyMap.Yes, q.keyMap.No},
139140 {q.keyMap.Tab, q.keyMap.Close},
internal/ui/model.godeleted−9
@@ -1,9 +0,0 @@
1−package ui
2−
3−import tea "github.com/charmbracelet/bubbletea/v2"
4−
5−// Model represents a common interface for UI components.
6−type Model[T any] interface {
7− Update(msg tea.Msg) (T, tea.Cmd)
8− View() string
9−}
internal/ui/keys.go → internal/ui/model/keys.gorenamed+1−1
@@ -1,4 +1,4 @@
1−package ui
1+package model
22
33import "github.com/charmbracelet/bubbles/v2/key"
44
internal/ui/ui.go → internal/ui/model/ui.gorenamed+32−7
@@ -1,4 +1,4 @@
1−package ui
1+package model
22
33import (
44 "image"
@@ -6,6 +6,8 @@ import (
66 "github.com/charmbracelet/bubbles/v2/key"
77 tea "github.com/charmbracelet/bubbletea/v2"
88 "github.com/charmbracelet/crush/internal/app"
9+ "github.com/charmbracelet/crush/internal/ui/common"
10+ "github.com/charmbracelet/crush/internal/ui/dialog"
911 "github.com/charmbracelet/lipgloss/v2"
1012 uv "github.com/charmbracelet/ultraviolet"
1113)
@@ -18,22 +20,21 @@ const (
1820
1921type UI struct {
2022 app *app.App
21− com *Common
23+ com *common.Common
2224
2325 width, height int
2426 state uiState
2527
2628 keyMap KeyMap
27− styles Styles
2829
29− dialog *Overlay
30+ dialog *dialog.Overlay
3031}
3132
32−func New(com *Common, app *app.App) *UI {
33+func New(com *common.Common, app *app.App) *UI {
3334 return &UI{
3435 app: app,
3536 com: com,
36− dialog: NewDialogOverlay(),
37+ dialog: dialog.NewOverlay(),
3738 keyMap: DefaultKeyMap(),
3839 }
3940}
@@ -53,7 +54,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5354 case uiStateMain:
5455 switch {
5556 case key.Matches(msg, m.keyMap.Quit):
56− quitDialog := NewQuitDialog(m.com)
57+ quitDialog := dialog.NewQuit(m.com)
5758 if !m.dialog.ContainsDialog(quitDialog.ID()) {
5859 m.dialog.AddDialog(quitDialog)
5960 return m, nil
@@ -73,6 +74,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7374
7475func (m *UI) View() tea.View {
7576 var v tea.View
77+ v.AltScreen = true
7678
7779 // The screen area we're working with
7880 area := image.Rect(0, 0, m.width, m.height)
@@ -88,6 +90,29 @@ func (m *UI) View() tea.View {
8890 )
8991 }
9092
93+ mainRect, sideRect := uv.SplitHorizontal(area, uv.Fixed(area.Dx()-40))
94+ mainRect, footRect := uv.SplitVertical(mainRect, uv.Fixed(area.Dy()-7))
95+
96+ layers = append(layers, lipgloss.NewLayer(
97+ lipgloss.NewStyle().Width(mainRect.Dx()).
98+ Height(mainRect.Dy()).
99+ Border(lipgloss.NormalBorder()).
100+ Render(" Main View "),
101+ ).X(mainRect.Min.X).Y(mainRect.Min.Y),
102+ lipgloss.NewLayer(
103+ lipgloss.NewStyle().Width(sideRect.Dx()).
104+ Height(sideRect.Dy()).
105+ Border(lipgloss.NormalBorder()).
106+ Render(" Side View "),
107+ ).X(sideRect.Min.X).Y(sideRect.Min.Y),
108+ lipgloss.NewLayer(
109+ lipgloss.NewStyle().Width(footRect.Dx()).
110+ Height(footRect.Dy()).
111+ Border(lipgloss.NormalBorder()).
112+ Render(" Footer View "),
113+ ).X(footRect.Min.X).Y(footRect.Min.Y),
114+ )
115+
91116 v.Layer = lipgloss.NewCanvas(layers...)
92117
93118 return v
internal/ui/styles.go → internal/ui/styles/styles.gorenamed+1−1
@@ -1,4 +1,4 @@
1−package ui
1+package styles
22
33import (
44 "github.com/charmbracelet/bubbles/v2/filepicker"