diff --git a/internal/cmd/root.go b/internal/cmd/root.go index fbd86bd7592d4bd8a65820d2131fdb9873de8200..9678633d0a661af3a7311d93c0ca341178a25c57 100644 --- a/internal/cmd/root.go +++ b/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()), diff --git a/internal/ui/common.go b/internal/ui/common/common.go similarity index 57% rename from internal/ui/common.go rename to internal/ui/common/common.go index 72a0bae02667aad6a0b7028b2e1fcdd017549137..3daec59f791c41ec281f718ef7ea434beb8cd7e8 100644 --- a/internal/ui/common.go +++ b/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(), } } diff --git a/internal/ui/dialog.go b/internal/ui/dialog/dialog.go similarity index 84% rename from internal/ui/dialog.go rename to internal/ui/dialog/dialog.go index bfae19d05b8f3fc53e7988c8c29ed2454ffb17a9..01815e24db73cb7e997c5fd935c0cfc822a2a9ac 100644 --- a/internal/ui/dialog.go +++ b/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(), } } diff --git a/internal/ui/quit_dialog.go b/internal/ui/dialog/quit.go similarity index 85% rename from internal/ui/quit_dialog.go rename to internal/ui/dialog/quit.go index b099ba671f1df17ef7d96c19a94ec91d50d29d24..37d089cbcec94e9082c6d9b8ac126abab0c8642b 100644 --- a/internal/ui/quit_dialog.go +++ b/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}, diff --git a/internal/ui/model.go b/internal/ui/model.go deleted file mode 100644 index ec8876b9adb643530d732a6f92bab5d28a8c1938..0000000000000000000000000000000000000000 --- a/internal/ui/model.go +++ /dev/null @@ -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 -} diff --git a/internal/ui/keys.go b/internal/ui/model/keys.go similarity index 98% rename from internal/ui/keys.go rename to internal/ui/model/keys.go index 335840fa06c7927588410c71140d3537881a434b..e0c1e0a6d96d0c3b624806146c80c5a668ef8aad 100644 --- a/internal/ui/keys.go +++ b/internal/ui/model/keys.go @@ -1,4 +1,4 @@ -package ui +package model import "github.com/charmbracelet/bubbles/v2/key" diff --git a/internal/ui/ui.go b/internal/ui/model/ui.go similarity index 65% rename from internal/ui/ui.go rename to internal/ui/model/ui.go index c09379203ae67bec3175d75de06b65df881c538d..fd5b8e16dc5cfb1bf7e8e281c36d4865b9b47d3f 100644 --- a/internal/ui/ui.go +++ b/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 diff --git a/internal/ui/styles.go b/internal/ui/styles/styles.go similarity index 99% rename from internal/ui/styles.go rename to internal/ui/styles/styles.go index 11a3871317b720f63b1819a1a7590e6282797e18..8a1dcc71d0a3462305dea97b04291a0be6a122f6 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles/styles.go @@ -1,4 +1,4 @@ -package ui +package styles import ( "github.com/charmbracelet/bubbles/v2/filepicker"