Detailed changes
@@ -0,0 +1,17 @@
+package ui
+
+import "github.com/charmbracelet/crush/internal/config"
+
+// Common defines common UI options and configurations.
+type Common struct {
+ Config *config.Config
+ Styles Styles
+}
+
+// DefaultCommon returns the default common UI configurations.
+func DefaultCommon(cfg *config.Config) *Common {
+ return &Common{
+ Config: cfg,
+ Styles: DefaultStyles(),
+ }
+}
@@ -1,29 +1,42 @@
-package dialog
+package ui
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"
)
-// Model is a component that can be displayed on top of the UI.
-type Model interface {
- common.Model[Model]
+// DialogOverlayKeyMap defines key bindings for dialogs.
+type DialogOverlayKeyMap struct {
+ Close key.Binding
+}
+
+// DefaultDialogOverlayKeyMap returns the default key bindings for dialogs.
+func DefaultDialogOverlayKeyMap() DialogOverlayKeyMap {
+ return DialogOverlayKeyMap{
+ Close: key.NewBinding(
+ key.WithKeys("esc", "alt+esc"),
+ ),
+ }
+}
+
+// Dialog is a component that can be displayed on top of the UI.
+type Dialog interface {
+ Model[Dialog]
ID() string
}
// Overlay manages multiple dialogs as an overlay.
type Overlay struct {
- dialogs []Model
- keyMap KeyMap
+ dialogs []Dialog
+ keyMap DialogOverlayKeyMap
}
-// NewOverlay creates a new [Overlay] instance.
-func NewOverlay(dialogs ...Model) *Overlay {
+// NewDialogOverlay creates a new [Overlay] instance.
+func NewDialogOverlay(dialogs ...Dialog) *Overlay {
return &Overlay{
dialogs: dialogs,
- keyMap: DefaultKeyMap(),
+ keyMap: DefaultDialogOverlayKeyMap(),
}
}
@@ -38,7 +51,7 @@ func (d *Overlay) ContainsDialog(dialogID string) bool {
}
// AddDialog adds a new dialog to the stack.
-func (d *Overlay) AddDialog(dialog Model) {
+func (d *Overlay) AddDialog(dialog Dialog) {
d.dialogs = append(d.dialogs, dialog)
}
@@ -1,57 +0,0 @@
-package dialog
-
-import "github.com/charmbracelet/bubbles/v2/key"
-
-// KeyMap defines key bindings for dialogs.
-type KeyMap struct {
- Close key.Binding
-}
-
-// DefaultKeyMap returns the default key bindings for dialogs.
-func DefaultKeyMap() KeyMap {
- return KeyMap{
- Close: key.NewBinding(
- key.WithKeys("esc", "alt+esc"),
- ),
- }
-}
-
-// QuitKeyMap represents key bindings for the quit dialog.
-type QuitKeyMap struct {
- LeftRight,
- EnterSpace,
- Yes,
- No,
- Tab,
- Close key.Binding
-}
-
-// DefaultQuitKeyMap returns the default key bindings for the quit dialog.
-func DefaultQuitKeyMap() QuitKeyMap {
- return QuitKeyMap{
- LeftRight: key.NewBinding(
- key.WithKeys("left", "right"),
- key.WithHelp("←/→", "switch options"),
- ),
- EnterSpace: key.NewBinding(
- key.WithKeys("enter", " "),
- key.WithHelp("enter/space", "confirm"),
- ),
- Yes: key.NewBinding(
- key.WithKeys("y", "Y", "ctrl+c"),
- key.WithHelp("y/Y/ctrl+c", "yes"),
- ),
- No: key.NewBinding(
- key.WithKeys("n", "N"),
- key.WithHelp("n/N", "no"),
- ),
- Tab: key.NewBinding(
- key.WithKeys("tab"),
- key.WithHelp("tab", "switch options"),
- ),
- Close: key.NewBinding(
- key.WithKeys("esc", "alt+esc"),
- key.WithHelp("esc", "cancel"),
- ),
- }
-}
@@ -1,4 +1,4 @@
-package common
+package ui
import tea "github.com/charmbracelet/bubbletea/v2"
@@ -1,4 +1,4 @@
-package dialog
+package ui
import (
"github.com/charmbracelet/bubbles/v2/key"
@@ -6,27 +6,69 @@ import (
"github.com/charmbracelet/lipgloss/v2"
)
-// Quit represents a confirmation dialog for quitting the application.
-type Quit struct {
- keyMap QuitKeyMap
+// QuitDialogKeyMap represents key bindings for the quit dialog.
+type QuitDialogKeyMap struct {
+ LeftRight,
+ EnterSpace,
+ Yes,
+ No,
+ Tab,
+ Close key.Binding
+}
+
+// DefaultQuitKeyMap returns the default key bindings for the quit dialog.
+func DefaultQuitKeyMap() QuitDialogKeyMap {
+ return QuitDialogKeyMap{
+ LeftRight: key.NewBinding(
+ key.WithKeys("left", "right"),
+ key.WithHelp("←/→", "switch options"),
+ ),
+ EnterSpace: key.NewBinding(
+ key.WithKeys("enter", " "),
+ key.WithHelp("enter/space", "confirm"),
+ ),
+ Yes: key.NewBinding(
+ key.WithKeys("y", "Y", "ctrl+c"),
+ key.WithHelp("y/Y/ctrl+c", "yes"),
+ ),
+ No: key.NewBinding(
+ key.WithKeys("n", "N"),
+ key.WithHelp("n/N", "no"),
+ ),
+ Tab: key.NewBinding(
+ key.WithKeys("tab"),
+ key.WithHelp("tab", "switch options"),
+ ),
+ Close: key.NewBinding(
+ key.WithKeys("esc", "alt+esc"),
+ key.WithHelp("esc", "cancel"),
+ ),
+ }
+}
+
+// QuitDialog represents a confirmation dialog for quitting the application.
+type QuitDialog struct {
+ com *Common
+ keyMap QuitDialogKeyMap
selectedNo bool // true if "No" button is selected
}
-// NewQuit creates a new quit confirmation dialog.
-func NewQuit() *Quit {
- q := &Quit{
+// NewQuitDialog creates a new quit confirmation dialog.
+func NewQuitDialog(com *Common) *QuitDialog {
+ q := &QuitDialog{
+ com: com,
keyMap: DefaultQuitKeyMap(),
}
return q
}
// ID implements [Model].
-func (*Quit) ID() string {
+func (*QuitDialog) ID() string {
return "quit"
}
// Update implements [Model].
-func (q *Quit) Update(msg tea.Msg) (Model, tea.Cmd) {
+func (q *QuitDialog) Update(msg tea.Msg) (Dialog, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
switch {
@@ -49,19 +91,15 @@ func (q *Quit) Update(msg tea.Msg) (Model, tea.Cmd) {
}
// View implements [Model].
-func (q *Quit) View() string {
+func (q *QuitDialog) View() string {
const question = "Are you sure you want to quit?"
-
- baseStyle := lipgloss.NewStyle()
- yesStyle := lipgloss.NewStyle()
- noStyle := yesStyle
+ baseStyle := q.com.Styles.Base
+ yesStyle := q.com.Styles.ButtonSelected
+ noStyle := q.com.Styles.ButtonUnselected
if q.selectedNo {
- noStyle = noStyle.Foreground(lipgloss.Color("15")).Background(lipgloss.Color("15"))
- yesStyle = yesStyle.Background(lipgloss.Color("15"))
- } else {
- yesStyle = yesStyle.Foreground(lipgloss.Color("15")).Background(lipgloss.Color("15"))
- noStyle = noStyle.Background(lipgloss.Color("15"))
+ noStyle = q.com.Styles.ButtonSelected
+ yesStyle = q.com.Styles.ButtonUnselected
}
const horizontalPadding = 3
@@ -83,16 +121,11 @@ func (q *Quit) View() string {
),
)
- quitDialogStyle := baseStyle.
- Padding(1, 2).
- Border(lipgloss.RoundedBorder()).
- BorderForeground(lipgloss.Color("15"))
-
- return quitDialogStyle.Render(content)
+ return q.com.Styles.BorderFocus.Render(content)
}
// ShortHelp implements [help.KeyMap].
-func (q *Quit) ShortHelp() []key.Binding {
+func (q *QuitDialog) ShortHelp() []key.Binding {
return []key.Binding{
q.keyMap.LeftRight,
q.keyMap.EnterSpace,
@@ -100,7 +133,7 @@ func (q *Quit) ShortHelp() []key.Binding {
}
// FullHelp implements [help.KeyMap].
-func (q *Quit) FullHelp() [][]key.Binding {
+func (q *QuitDialog) FullHelp() [][]key.Binding {
return [][]key.Binding{
{q.keyMap.LeftRight, q.keyMap.EnterSpace, q.keyMap.Yes, q.keyMap.No},
{q.keyMap.Tab, q.keyMap.Close},
@@ -0,0 +1,194 @@
+package ui
+
+import (
+ "github.com/charmbracelet/bubbles/v2/filepicker"
+ "github.com/charmbracelet/bubbles/v2/help"
+ "github.com/charmbracelet/bubbles/v2/textarea"
+ "github.com/charmbracelet/bubbles/v2/textinput"
+ "github.com/charmbracelet/crush/internal/tui/exp/diffview"
+ "github.com/charmbracelet/glamour/v2/ansi"
+ "github.com/charmbracelet/lipgloss/v2"
+ "github.com/charmbracelet/x/exp/charmtone"
+)
+
+const (
+ CheckIcon string = "✓"
+ ErrorIcon string = "×"
+ WarningIcon string = "⚠"
+ InfoIcon string = "ⓘ"
+ HintIcon string = "∵"
+ SpinnerIcon string = "..."
+ LoadingIcon string = "⟳"
+ DocumentIcon string = "🖼"
+ ModelIcon string = "◇"
+
+ ToolPending string = "●"
+ ToolSuccess string = "✓"
+ ToolError string = "×"
+
+ BorderThin string = "│"
+ BorderThick string = "▌"
+)
+
+type Styles struct {
+ WindowTooSmall lipgloss.Style
+
+ // Reusable text styles
+ Base lipgloss.Style
+ Muted lipgloss.Style
+ Subtle lipgloss.Style
+
+ // Tags
+ TagBase lipgloss.Style
+ TagError lipgloss.Style
+ TagInfo lipgloss.Style
+
+ // Headers
+ HeaderTool lipgloss.Style
+ HeaderToolNested lipgloss.Style
+
+ // Panels
+ PanelMuted lipgloss.Style
+ PanelBase lipgloss.Style
+
+ // Line numbers for code blocks
+ LineNumber lipgloss.Style
+
+ // Message borders
+ FocusedMessageBorder lipgloss.Border
+
+ // Tool calls
+ ToolCallPending lipgloss.Style
+ ToolCallError lipgloss.Style
+ ToolCallSuccess lipgloss.Style
+ ToolCallCancelled lipgloss.Style
+ EarlyStateMessage lipgloss.Style
+
+ // Text selection
+ TextSelection lipgloss.Style
+
+ // LSP and MCP status indicators
+ ItemOfflineIcon lipgloss.Style
+ ItemBusyIcon lipgloss.Style
+ ItemErrorIcon lipgloss.Style
+ ItemOnlineIcon lipgloss.Style
+
+ // Markdown & Chroma
+ Markdown ansi.StyleConfig
+
+ // Inputs
+ TextInput textinput.Styles
+ TextArea textarea.Styles
+
+ // Help
+ Help help.Styles
+
+ // Diff
+ Diff diffview.Style
+
+ // FilePicker
+ FilePicker filepicker.Styles
+
+ // Buttons
+ ButtonSelected lipgloss.Style
+ ButtonUnselected lipgloss.Style
+
+ // Borders
+ BorderFocus lipgloss.Style
+ BorderBlur lipgloss.Style
+}
+
+func DefaultStyles() Styles {
+ var (
+ // primary = charmtone.Charple
+ secondary = charmtone.Dolly
+ // tertiary = charmtone.Bok
+ // accent = charmtone.Zest
+
+ // Backgrounds
+ bgBase = charmtone.Pepper
+ bgBaseLighter = charmtone.BBQ
+ bgSubtle = charmtone.Charcoal
+ // bgOverlay = charmtone.Iron
+
+ // Foregrounds
+ fgBase = charmtone.Ash
+ fgMuted = charmtone.Squid
+ fgHalfMuted = charmtone.Smoke
+ fgSubtle = charmtone.Oyster
+ // fgSelected = charmtone.Salt
+
+ // Borders
+ // border = charmtone.Charcoal
+ borderFocus = charmtone.Charple
+
+ // Status
+ // success = charmtone.Guac
+ // error = charmtone.Sriracha
+ // warning = charmtone.Zest
+ // info = charmtone.Malibu
+
+ // Colors
+ white = charmtone.Butter
+
+ blueLight = charmtone.Sardine
+ blue = charmtone.Malibu
+
+ // yellow = charmtone.Mustard
+ // citron = charmtone.Citron
+
+ green = charmtone.Julep
+ greenDark = charmtone.Guac
+ // greenLight = charmtone.Bok
+
+ // red = charmtone.Coral
+ redDark = charmtone.Sriracha
+ // redLight = charmtone.Salmon
+ // cherry = charmtone.Cherry
+ )
+
+ s := Styles{}
+
+ // borders
+ s.FocusedMessageBorder = lipgloss.Border{Left: BorderThick}
+
+ // text presets
+ s.Base = lipgloss.NewStyle().Foreground(fgBase)
+ s.Muted = lipgloss.NewStyle().Foreground(fgMuted)
+ s.Subtle = lipgloss.NewStyle().Foreground(fgSubtle)
+
+ s.WindowTooSmall = s.Muted
+
+ // tag presets
+ s.TagBase = lipgloss.NewStyle().Padding(0, 1).Foreground(white)
+ s.TagError = s.TagBase.Background(redDark)
+ s.TagInfo = s.TagBase.Background(blueLight)
+
+ // headers
+ s.HeaderTool = lipgloss.NewStyle().Foreground(blue)
+ s.HeaderToolNested = lipgloss.NewStyle().Foreground(fgHalfMuted)
+
+ // panels
+ s.PanelMuted = s.Muted.Background(bgBaseLighter)
+ s.PanelBase = lipgloss.NewStyle().Background(bgBase)
+
+ // code line number
+ s.LineNumber = lipgloss.NewStyle().Foreground(fgMuted).Background(bgBase).PaddingRight(1).PaddingLeft(1)
+
+ // Tool calls
+ s.ToolCallPending = lipgloss.NewStyle().Foreground(greenDark).SetString(ToolPending)
+ s.ToolCallError = lipgloss.NewStyle().Foreground(redDark).SetString(ToolError)
+ s.ToolCallSuccess = lipgloss.NewStyle().Foreground(green).SetString(ToolSuccess)
+ // Cancelled uses muted tone but same glyph as pending
+ s.ToolCallCancelled = s.Muted.SetString(ToolPending)
+ s.EarlyStateMessage = s.Subtle.PaddingLeft(2)
+
+ // Buttons
+ s.ButtonSelected = lipgloss.NewStyle().Foreground(white).Background(secondary)
+ s.ButtonUnselected = s.Base.Background(bgSubtle)
+
+ // Borders
+ s.BorderFocus = lipgloss.NewStyle().BorderForeground(borderFocus).Border(lipgloss.RoundedBorder()).Padding(1, 2)
+
+ return s
+}
@@ -6,7 +6,6 @@ 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/dialog"
"github.com/charmbracelet/lipgloss/v2"
uv "github.com/charmbracelet/ultraviolet"
)
@@ -17,29 +16,33 @@ const (
uiStateMain uiState = iota
)
-type Model struct {
- app *app.App
+type UI struct {
+ app *app.App
+ com *Common
+
width, height int
state uiState
keyMap KeyMap
+ styles Styles
- dialog *dialog.Overlay
+ dialog *Overlay
}
-func New(app *app.App) *Model {
- return &Model{
+func New(com *Common, app *app.App) *UI {
+ return &UI{
app: app,
- dialog: dialog.NewOverlay(),
+ com: com,
+ dialog: NewDialogOverlay(),
keyMap: DefaultKeyMap(),
}
}
-func (m *Model) Init() tea.Cmd {
+func (m *UI) Init() tea.Cmd {
return nil
}
-func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
@@ -50,7 +53,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case uiStateMain:
switch {
case key.Matches(msg, m.keyMap.Quit):
- quitDialog := dialog.NewQuit()
+ quitDialog := NewQuitDialog(m.com)
if !m.dialog.ContainsDialog(quitDialog.ID()) {
m.dialog.AddDialog(quitDialog)
return m, nil
@@ -68,7 +71,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}
-func (m *Model) View() tea.View {
+func (m *UI) View() tea.View {
var v tea.View
// The screen area we're working with