on main help sign in
feat(ui): restructure UI package into subpackages
8783795Ayman Bagabas
internal/cmd/root.go+4−2
@@ -18,7 +18,8 @@ import (
18 "github.com/charmbracelet/crush/internal/db"
18 "github.com/charmbracelet/crush/internal/db"
19 "github.com/charmbracelet/crush/internal/event"
19 "github.com/charmbracelet/crush/internal/event"
20 "github.com/charmbracelet/crush/internal/tui"
20 "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"
22 "github.com/charmbracelet/crush/internal/version"
23 "github.com/charmbracelet/crush/internal/version"
23 "github.com/charmbracelet/fang"
24 "github.com/charmbracelet/fang"
24 "github.com/charmbracelet/lipgloss/v2"
25 "github.com/charmbracelet/lipgloss/v2"
@@ -83,7 +84,8 @@ crush -y
83
84
84 // Set up the TUI.
85 // Set up the TUI.
85 // ui := tui.New(app)
86 // ui := tui.New(app)
87+ com := common.DefaultCommon(app.Config())
86− ui := ui.New(app)
88+ ui := ui.New(com, app)
87 program := tea.NewProgram(
89 program := tea.NewProgram(
88 ui,
90 ui,
89 tea.WithContext(cmd.Context()),
91 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
2
2
3+import (
3−import "github.com/charmbracelet/crush/internal/config"
4+ "github.com/charmbracelet/crush/internal/config"
5+ "github.com/charmbracelet/crush/internal/ui/styles"
6+)
4
7
5// Common defines common UI options and configurations.
8// Common defines common UI options and configurations.
6type Common struct {
9type Common struct {
7 Config *config.Config
10 Config *config.Config
8− Styles Styles
11+ Styles styles.Styles
9}
12}
10
13
11// DefaultCommon returns the default common UI configurations.
14// DefaultCommon returns the default common UI configurations.
12func DefaultCommon(cfg *config.Config) *Common {
15func DefaultCommon(cfg *config.Config) *Common {
13 return &Common{
16 return &Common{
14 Config: cfg,
17 Config: cfg,
15− Styles: DefaultStyles(),
18+ Styles: styles.DefaultStyles(),
16 }
19 }
17}
20}
internal/ui/dialog.go → internal/ui/dialog/dialog.gorenamed+12−11
@@ -1,19 +1,20 @@
1−package ui
1+package dialog
2
2
3import (
3import (
4 "github.com/charmbracelet/bubbles/v2/key"
4 "github.com/charmbracelet/bubbles/v2/key"
5 tea "github.com/charmbracelet/bubbletea/v2"
5 tea "github.com/charmbracelet/bubbletea/v2"
6+ "github.com/charmbracelet/crush/internal/ui/component"
6 "github.com/charmbracelet/lipgloss/v2"
7 "github.com/charmbracelet/lipgloss/v2"
7)
8)
8
9
9−// DialogOverlayKeyMap defines key bindings for dialogs.
10+// OverlayKeyMap defines key bindings for dialogs.
10−type DialogOverlayKeyMap struct {
11+type OverlayKeyMap struct {
11 Close key.Binding
12 Close key.Binding
12}
13}
13
14
14−// DefaultDialogOverlayKeyMap returns the default key bindings for dialogs.
15+// DefaultOverlayKeyMap returns the default key bindings for dialogs.
15−func DefaultDialogOverlayKeyMap() DialogOverlayKeyMap {
16+func DefaultOverlayKeyMap() OverlayKeyMap {
16− return DialogOverlayKeyMap{
17+ return OverlayKeyMap{
17 Close: key.NewBinding(
18 Close: key.NewBinding(
18 key.WithKeys("esc", "alt+esc"),
19 key.WithKeys("esc", "alt+esc"),
19 ),
20 ),
@@ -22,21 +23,21 @@ func DefaultDialogOverlayKeyMap() DialogOverlayKeyMap {
22
23
23// Dialog is a component that can be displayed on top of the UI.
24// Dialog is a component that can be displayed on top of the UI.
24type Dialog interface {
25type Dialog interface {
25− Model[Dialog]
26+ component.Model[Dialog]
26 ID() string
27 ID() string
27}
28}
28
29
29// Overlay manages multiple dialogs as an overlay.
30// Overlay manages multiple dialogs as an overlay.
30type Overlay struct {
31type Overlay struct {
31 dialogs []Dialog
32 dialogs []Dialog
32− keyMap DialogOverlayKeyMap
33+ keyMap OverlayKeyMap
33}
34}
34
35
35−// NewDialogOverlay creates a new [Overlay] instance.
36+// NewOverlay creates a new [Overlay] instance.
36−func NewDialogOverlay(dialogs ...Dialog) *Overlay {
37+func NewOverlay(dialogs ...Dialog) *Overlay {
37 return &Overlay{
38 return &Overlay{
38 dialogs: dialogs,
39 dialogs: dialogs,
39− keyMap: DefaultDialogOverlayKeyMap(),
40+ keyMap: DefaultOverlayKeyMap(),
40 }
41 }
41}
42}
42
43
internal/ui/quit_dialog.go → internal/ui/dialog/quit.gorenamed+13−12
@@ -1,8 +1,9 @@
1−package ui
1+package dialog
2
2
3import (
3import (
4 "github.com/charmbracelet/bubbles/v2/key"
4 "github.com/charmbracelet/bubbles/v2/key"
5 tea "github.com/charmbracelet/bubbletea/v2"
5 tea "github.com/charmbracelet/bubbletea/v2"
6+ "github.com/charmbracelet/crush/internal/ui/common"
6 "github.com/charmbracelet/lipgloss/v2"
7 "github.com/charmbracelet/lipgloss/v2"
7)
8)
8
9
@@ -46,16 +47,16 @@ func DefaultQuitKeyMap() QuitDialogKeyMap {
46 }
47 }
47}
48}
48
49
49−// QuitDialog represents a confirmation dialog for quitting the application.
50+// Quit represents a confirmation dialog for quitting the application.
50−type QuitDialog struct {
51+type Quit struct {
51− com *Common
52+ com *common.Common
52 keyMap QuitDialogKeyMap
53 keyMap QuitDialogKeyMap
53 selectedNo bool // true if "No" button is selected
54 selectedNo bool // true if "No" button is selected
54}
55}
55
56
56−// NewQuitDialog creates a new quit confirmation dialog.
57+// NewQuit creates a new quit confirmation dialog.
57−func NewQuitDialog(com *Common) *QuitDialog {
58+func NewQuit(com *common.Common) *Quit {
58− q := &QuitDialog{
59+ q := &Quit{
59 com: com,
60 com: com,
60 keyMap: DefaultQuitKeyMap(),
61 keyMap: DefaultQuitKeyMap(),
61 }
62 }
@@ -63,12 +64,12 @@ func NewQuitDialog(com *Common) *QuitDialog {
63}
64}
64
65
65// ID implements [Model].
66// ID implements [Model].
66−func (*QuitDialog) ID() string {
67+func (*Quit) ID() string {
67 return "quit"
68 return "quit"
68}
69}
69
70
70// Update implements [Model].
71// 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) {
72 switch msg := msg.(type) {
73 switch msg := msg.(type) {
73 case tea.KeyPressMsg:
74 case tea.KeyPressMsg:
74 switch {
75 switch {
@@ -91,7 +92,7 @@ func (q *QuitDialog) Update(msg tea.Msg) (Dialog, tea.Cmd) {
91}
92}
92
93
93// View implements [Model].
94// View implements [Model].
94−func (q *QuitDialog) View() string {
95+func (q *Quit) View() string {
95 const question = "Are you sure you want to quit?"
96 const question = "Are you sure you want to quit?"
96 baseStyle := q.com.Styles.Base
97 baseStyle := q.com.Styles.Base
97 yesStyle := q.com.Styles.ButtonSelected
98 yesStyle := q.com.Styles.ButtonSelected
@@ -125,7 +126,7 @@ func (q *QuitDialog) View() string {
125}
126}
126
127
127// ShortHelp implements [help.KeyMap].
128// ShortHelp implements [help.KeyMap].
128−func (q *QuitDialog) ShortHelp() []key.Binding {
129+func (q *Quit) ShortHelp() []key.Binding {
129 return []key.Binding{
130 return []key.Binding{
130 q.keyMap.LeftRight,
131 q.keyMap.LeftRight,
131 q.keyMap.EnterSpace,
132 q.keyMap.EnterSpace,
@@ -133,7 +134,7 @@ func (q *QuitDialog) ShortHelp() []key.Binding {
133}
134}
134
135
135// FullHelp implements [help.KeyMap].
136// FullHelp implements [help.KeyMap].
136−func (q *QuitDialog) FullHelp() [][]key.Binding {
137+func (q *Quit) FullHelp() [][]key.Binding {
137 return [][]key.Binding{
138 return [][]key.Binding{
138 {q.keyMap.LeftRight, q.keyMap.EnterSpace, q.keyMap.Yes, q.keyMap.No},
139 {q.keyMap.LeftRight, q.keyMap.EnterSpace, q.keyMap.Yes, q.keyMap.No},
139 {q.keyMap.Tab, q.keyMap.Close},
140 {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
2
2
3import "github.com/charmbracelet/bubbles/v2/key"
3import "github.com/charmbracelet/bubbles/v2/key"
4
4
internal/ui/ui.go → internal/ui/model/ui.gorenamed+32−7
@@ -1,4 +1,4 @@
1−package ui
1+package model
2
2
3import (
3import (
4 "image"
4 "image"
@@ -6,6 +6,8 @@ import (
6 "github.com/charmbracelet/bubbles/v2/key"
6 "github.com/charmbracelet/bubbles/v2/key"
7 tea "github.com/charmbracelet/bubbletea/v2"
7 tea "github.com/charmbracelet/bubbletea/v2"
8 "github.com/charmbracelet/crush/internal/app"
8 "github.com/charmbracelet/crush/internal/app"
9+ "github.com/charmbracelet/crush/internal/ui/common"
10+ "github.com/charmbracelet/crush/internal/ui/dialog"
9 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/lipgloss/v2"
10 uv "github.com/charmbracelet/ultraviolet"
12 uv "github.com/charmbracelet/ultraviolet"
11)
13)
@@ -18,22 +20,21 @@ const (
18
20
19type UI struct {
21type UI struct {
20 app *app.App
22 app *app.App
21− com *Common
23+ com *common.Common
22
24
23 width, height int
25 width, height int
24 state uiState
26 state uiState
25
27
26 keyMap KeyMap
28 keyMap KeyMap
27− styles Styles
28
29
29− dialog *Overlay
30+ dialog *dialog.Overlay
30}
31}
31
32
32−func New(com *Common, app *app.App) *UI {
33+func New(com *common.Common, app *app.App) *UI {
33 return &UI{
34 return &UI{
34 app: app,
35 app: app,
35 com: com,
36 com: com,
36− dialog: NewDialogOverlay(),
37+ dialog: dialog.NewOverlay(),
37 keyMap: DefaultKeyMap(),
38 keyMap: DefaultKeyMap(),
38 }
39 }
39}
40}
@@ -53,7 +54,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
53 case uiStateMain:
54 case uiStateMain:
54 switch {
55 switch {
55 case key.Matches(msg, m.keyMap.Quit):
56 case key.Matches(msg, m.keyMap.Quit):
56− quitDialog := NewQuitDialog(m.com)
57+ quitDialog := dialog.NewQuit(m.com)
57 if !m.dialog.ContainsDialog(quitDialog.ID()) {
58 if !m.dialog.ContainsDialog(quitDialog.ID()) {
58 m.dialog.AddDialog(quitDialog)
59 m.dialog.AddDialog(quitDialog)
59 return m, nil
60 return m, nil
@@ -73,6 +74,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
73
74
74func (m *UI) View() tea.View {
75func (m *UI) View() tea.View {
75 var v tea.View
76 var v tea.View
77+ v.AltScreen = true
76
78
77 // The screen area we're working with
79 // The screen area we're working with
78 area := image.Rect(0, 0, m.width, m.height)
80 area := image.Rect(0, 0, m.width, m.height)
@@ -88,6 +90,29 @@ func (m *UI) View() tea.View {
88 )
90 )
89 }
91 }
90
92
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+
91 v.Layer = lipgloss.NewCanvas(layers...)
116 v.Layer = lipgloss.NewCanvas(layers...)
92
117
93 return v
118 return v
internal/ui/styles.go → internal/ui/styles/styles.gorenamed+1−1
@@ -1,4 +1,4 @@
1−package ui
1+package styles
2
2
3import (
3import (
4 "github.com/charmbracelet/bubbles/v2/filepicker"
4 "github.com/charmbracelet/bubbles/v2/filepicker"