feat(ui): restructure UI package into subpackages

Ayman Bagabas created

Change summary

internal/cmd/root.go         |  6 +++-
internal/ui/common/common.go | 11 ++++++---
internal/ui/dialog/dialog.go | 23 +++++++++++----------
internal/ui/dialog/quit.go   | 25 ++++++++++++-----------
internal/ui/model.go         |  9 --------
internal/ui/model/keys.go    |  2 
internal/ui/model/ui.go      | 39 +++++++++++++++++++++++++++++++------
internal/ui/styles/styles.go |  2 
8 files changed, 70 insertions(+), 47 deletions(-)

Detailed changes

internal/cmd/root.go 🔗

@@ -18,7 +18,8 @@ import (
 	"github.com/charmbracelet/crush/internal/db"
 	"github.com/charmbracelet/crush/internal/event"
 	"github.com/charmbracelet/crush/internal/tui"
-	"github.com/charmbracelet/crush/internal/ui"
+	"github.com/charmbracelet/crush/internal/ui/common"
+	ui "github.com/charmbracelet/crush/internal/ui/model"
 	"github.com/charmbracelet/crush/internal/version"
 	"github.com/charmbracelet/fang"
 	"github.com/charmbracelet/lipgloss/v2"
@@ -83,7 +84,8 @@ crush -y
 
 		// Set up the TUI.
 		// ui := tui.New(app)
-		ui := ui.New(app)
+		com := common.DefaultCommon(app.Config())
+		ui := ui.New(com, app)
 		program := tea.NewProgram(
 			ui,
 			tea.WithContext(cmd.Context()),

internal/ui/common.go → internal/ui/common/common.go 🔗

@@ -1,17 +1,20 @@
-package ui
+package common
 
-import "github.com/charmbracelet/crush/internal/config"
+import (
+	"github.com/charmbracelet/crush/internal/config"
+	"github.com/charmbracelet/crush/internal/ui/styles"
+)
 
 // Common defines common UI options and configurations.
 type Common struct {
 	Config *config.Config
-	Styles Styles
+	Styles styles.Styles
 }
 
 // DefaultCommon returns the default common UI configurations.
 func DefaultCommon(cfg *config.Config) *Common {
 	return &Common{
 		Config: cfg,
-		Styles: DefaultStyles(),
+		Styles: styles.DefaultStyles(),
 	}
 }

internal/ui/dialog.go → internal/ui/dialog/dialog.go 🔗

@@ -1,19 +1,20 @@
-package ui
+package dialog
 
 import (
 	"github.com/charmbracelet/bubbles/v2/key"
 	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/crush/internal/ui/component"
 	"github.com/charmbracelet/lipgloss/v2"
 )
 
-// DialogOverlayKeyMap defines key bindings for dialogs.
-type DialogOverlayKeyMap struct {
+// OverlayKeyMap defines key bindings for dialogs.
+type OverlayKeyMap struct {
 	Close key.Binding
 }
 
-// DefaultDialogOverlayKeyMap returns the default key bindings for dialogs.
-func DefaultDialogOverlayKeyMap() DialogOverlayKeyMap {
-	return DialogOverlayKeyMap{
+// DefaultOverlayKeyMap returns the default key bindings for dialogs.
+func DefaultOverlayKeyMap() OverlayKeyMap {
+	return OverlayKeyMap{
 		Close: key.NewBinding(
 			key.WithKeys("esc", "alt+esc"),
 		),
@@ -22,21 +23,21 @@ func DefaultDialogOverlayKeyMap() DialogOverlayKeyMap {
 
 // Dialog is a component that can be displayed on top of the UI.
 type Dialog interface {
-	Model[Dialog]
+	component.Model[Dialog]
 	ID() string
 }
 
 // Overlay manages multiple dialogs as an overlay.
 type Overlay struct {
 	dialogs []Dialog
-	keyMap  DialogOverlayKeyMap
+	keyMap  OverlayKeyMap
 }
 
-// NewDialogOverlay creates a new [Overlay] instance.
-func NewDialogOverlay(dialogs ...Dialog) *Overlay {
+// NewOverlay creates a new [Overlay] instance.
+func NewOverlay(dialogs ...Dialog) *Overlay {
 	return &Overlay{
 		dialogs: dialogs,
-		keyMap:  DefaultDialogOverlayKeyMap(),
+		keyMap:  DefaultOverlayKeyMap(),
 	}
 }
 

internal/ui/quit_dialog.go → internal/ui/dialog/quit.go 🔗

@@ -1,8 +1,9 @@
-package ui
+package dialog
 
 import (
 	"github.com/charmbracelet/bubbles/v2/key"
 	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/crush/internal/ui/common"
 	"github.com/charmbracelet/lipgloss/v2"
 )
 
@@ -46,16 +47,16 @@ func DefaultQuitKeyMap() QuitDialogKeyMap {
 	}
 }
 
-// QuitDialog represents a confirmation dialog for quitting the application.
-type QuitDialog struct {
-	com        *Common
+// Quit represents a confirmation dialog for quitting the application.
+type Quit struct {
+	com        *common.Common
 	keyMap     QuitDialogKeyMap
 	selectedNo bool // true if "No" button is selected
 }
 
-// NewQuitDialog creates a new quit confirmation dialog.
-func NewQuitDialog(com *Common) *QuitDialog {
-	q := &QuitDialog{
+// NewQuit creates a new quit confirmation dialog.
+func NewQuit(com *common.Common) *Quit {
+	q := &Quit{
 		com:    com,
 		keyMap: DefaultQuitKeyMap(),
 	}
@@ -63,12 +64,12 @@ func NewQuitDialog(com *Common) *QuitDialog {
 }
 
 // ID implements [Model].
-func (*QuitDialog) ID() string {
+func (*Quit) ID() string {
 	return "quit"
 }
 
 // Update implements [Model].
-func (q *QuitDialog) Update(msg tea.Msg) (Dialog, tea.Cmd) {
+func (q *Quit) Update(msg tea.Msg) (Dialog, tea.Cmd) {
 	switch msg := msg.(type) {
 	case tea.KeyPressMsg:
 		switch {
@@ -91,7 +92,7 @@ func (q *QuitDialog) Update(msg tea.Msg) (Dialog, tea.Cmd) {
 }
 
 // View implements [Model].
-func (q *QuitDialog) View() string {
+func (q *Quit) View() string {
 	const question = "Are you sure you want to quit?"
 	baseStyle := q.com.Styles.Base
 	yesStyle := q.com.Styles.ButtonSelected
@@ -125,7 +126,7 @@ func (q *QuitDialog) View() string {
 }
 
 // ShortHelp implements [help.KeyMap].
-func (q *QuitDialog) ShortHelp() []key.Binding {
+func (q *Quit) ShortHelp() []key.Binding {
 	return []key.Binding{
 		q.keyMap.LeftRight,
 		q.keyMap.EnterSpace,
@@ -133,7 +134,7 @@ func (q *QuitDialog) ShortHelp() []key.Binding {
 }
 
 // FullHelp implements [help.KeyMap].
-func (q *QuitDialog) FullHelp() [][]key.Binding {
+func (q *Quit) FullHelp() [][]key.Binding {
 	return [][]key.Binding{
 		{q.keyMap.LeftRight, q.keyMap.EnterSpace, q.keyMap.Yes, q.keyMap.No},
 		{q.keyMap.Tab, q.keyMap.Close},

internal/ui/model.go 🔗

@@ -1,9 +0,0 @@
-package ui
-
-import tea "github.com/charmbracelet/bubbletea/v2"
-
-// Model represents a common interface for UI components.
-type Model[T any] interface {
-	Update(msg tea.Msg) (T, tea.Cmd)
-	View() string
-}

internal/ui/keys.go → internal/ui/model/keys.go 🔗

@@ -1,4 +1,4 @@
-package ui
+package model
 
 import "github.com/charmbracelet/bubbles/v2/key"
 

internal/ui/ui.go → internal/ui/model/ui.go 🔗

@@ -1,4 +1,4 @@
-package ui
+package model
 
 import (
 	"image"
@@ -6,6 +6,8 @@ import (
 	"github.com/charmbracelet/bubbles/v2/key"
 	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/crush/internal/app"
+	"github.com/charmbracelet/crush/internal/ui/common"
+	"github.com/charmbracelet/crush/internal/ui/dialog"
 	"github.com/charmbracelet/lipgloss/v2"
 	uv "github.com/charmbracelet/ultraviolet"
 )
@@ -18,22 +20,21 @@ const (
 
 type UI struct {
 	app *app.App
-	com *Common
+	com *common.Common
 
 	width, height int
 	state         uiState
 
 	keyMap KeyMap
-	styles Styles
 
-	dialog *Overlay
+	dialog *dialog.Overlay
 }
 
-func New(com *Common, app *app.App) *UI {
+func New(com *common.Common, app *app.App) *UI {
 	return &UI{
 		app:    app,
 		com:    com,
-		dialog: NewDialogOverlay(),
+		dialog: dialog.NewOverlay(),
 		keyMap: DefaultKeyMap(),
 	}
 }
@@ -53,7 +54,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		case uiStateMain:
 			switch {
 			case key.Matches(msg, m.keyMap.Quit):
-				quitDialog := NewQuitDialog(m.com)
+				quitDialog := dialog.NewQuit(m.com)
 				if !m.dialog.ContainsDialog(quitDialog.ID()) {
 					m.dialog.AddDialog(quitDialog)
 					return m, nil
@@ -73,6 +74,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 func (m *UI) View() tea.View {
 	var v tea.View
+	v.AltScreen = true
 
 	// The screen area we're working with
 	area := image.Rect(0, 0, m.width, m.height)
@@ -88,6 +90,29 @@ func (m *UI) View() tea.View {
 		)
 	}
 
+	mainRect, sideRect := uv.SplitHorizontal(area, uv.Fixed(area.Dx()-40))
+	mainRect, footRect := uv.SplitVertical(mainRect, uv.Fixed(area.Dy()-7))
+
+	layers = append(layers, lipgloss.NewLayer(
+		lipgloss.NewStyle().Width(mainRect.Dx()).
+			Height(mainRect.Dy()).
+			Border(lipgloss.NormalBorder()).
+			Render(" Main View "),
+	).X(mainRect.Min.X).Y(mainRect.Min.Y),
+		lipgloss.NewLayer(
+			lipgloss.NewStyle().Width(sideRect.Dx()).
+				Height(sideRect.Dy()).
+				Border(lipgloss.NormalBorder()).
+				Render(" Side View "),
+		).X(sideRect.Min.X).Y(sideRect.Min.Y),
+		lipgloss.NewLayer(
+			lipgloss.NewStyle().Width(footRect.Dx()).
+				Height(footRect.Dy()).
+				Border(lipgloss.NormalBorder()).
+				Render(" Footer View "),
+		).X(footRect.Min.X).Y(footRect.Min.Y),
+	)
+
 	v.Layer = lipgloss.NewCanvas(layers...)
 
 	return v

internal/ui/styles.go → internal/ui/styles/styles.go 🔗

@@ -1,4 +1,4 @@
-package ui
+package styles
 
 import (
 	"github.com/charmbracelet/bubbles/v2/filepicker"