diff --git a/internal/ui/common/common.go b/internal/ui/common/common.go index 3daec59f791c41ec281f718ef7ea434beb8cd7e8..88ca494ad5ac885806fe0d8959ae8b5e4b5592f9 100644 --- a/internal/ui/common/common.go +++ b/internal/ui/common/common.go @@ -1,8 +1,11 @@ package common import ( + "image" + "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/ui/styles" + uv "github.com/charmbracelet/ultraviolet" ) // Common defines common UI options and configurations. @@ -18,3 +21,15 @@ func DefaultCommon(cfg *config.Config) *Common { Styles: styles.DefaultStyles(), } } + +// CenterRect returns a new [Rectangle] centered within the given area with the +// specified width and height. +func CenterRect(area uv.Rectangle, width, height int) uv.Rectangle { + centerX := area.Min.X + area.Dx()/2 + centerY := area.Min.Y + area.Dy()/2 + minX := centerX - width/2 + minY := centerY - height/2 + maxX := minX + width + maxY := minY + height + return image.Rect(minX, minY, maxX, maxY) +} diff --git a/internal/ui/common/interface.go b/internal/ui/common/interface.go new file mode 100644 index 0000000000000000000000000000000000000000..aee3a1f081b965fbccb2f98e6d83846c122d3e5c --- /dev/null +++ b/internal/ui/common/interface.go @@ -0,0 +1,11 @@ +package common + +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/dialog/dialog.go b/internal/ui/dialog/dialog.go index 01815e24db73cb7e997c5fd935c0cfc822a2a9ac..f9fcf6d89a9b7168315f42bb53dabecfc63f7647 100644 --- a/internal/ui/dialog/dialog.go +++ b/internal/ui/dialog/dialog.go @@ -3,7 +3,7 @@ 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/crush/internal/ui/common" "github.com/charmbracelet/lipgloss/v2" ) @@ -23,7 +23,7 @@ func DefaultOverlayKeyMap() OverlayKeyMap { // Dialog is a component that can be displayed on top of the UI. type Dialog interface { - component.Model[Dialog] + common.Model[Dialog] ID() string } @@ -41,6 +41,11 @@ func NewOverlay(dialogs ...Dialog) *Overlay { } } +// HasDialogs checks if there are any active dialogs. +func (d *Overlay) HasDialogs() bool { + return len(d.dialogs) > 0 +} + // ContainsDialog checks if a dialog with the specified ID exists. func (d *Overlay) ContainsDialog(dialogID string) bool { for _, dialog := range d.dialogs { diff --git a/internal/ui/dialog/quit.go b/internal/ui/dialog/quit.go index 37d089cbcec94e9082c6d9b8ac126abab0c8642b..79c5d219360279c2758fcea227cbc3c409f2be0f 100644 --- a/internal/ui/dialog/quit.go +++ b/internal/ui/dialog/quit.go @@ -7,6 +7,9 @@ import ( "github.com/charmbracelet/lipgloss/v2" ) +// QuitDialogID is the identifier for the quit dialog. +const QuitDialogID = "quit" + // QuitDialogKeyMap represents key bindings for the quit dialog. type QuitDialogKeyMap struct { LeftRight, @@ -65,7 +68,7 @@ func NewQuit(com *common.Common) *Quit { // ID implements [Model]. func (*Quit) ID() string { - return "quit" + return QuitDialogID } // Update implements [Model]. diff --git a/internal/ui/model/chat.go b/internal/ui/model/chat.go new file mode 100644 index 0000000000000000000000000000000000000000..f19c2cac85eee946225e0041013a978b53a76f15 --- /dev/null +++ b/internal/ui/model/chat.go @@ -0,0 +1,86 @@ +package model + +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" +) + +// ChatKeyMap defines key bindings for the chat model. +type ChatKeyMap struct { + NewSession key.Binding + AddAttachment key.Binding + Cancel key.Binding + Tab key.Binding + Details key.Binding +} + +// DefaultChatKeyMap returns the default key bindings for the chat model. +func DefaultChatKeyMap() ChatKeyMap { + return ChatKeyMap{ + NewSession: key.NewBinding( + key.WithKeys("ctrl+n"), + key.WithHelp("ctrl+n", "new session"), + ), + AddAttachment: key.NewBinding( + key.WithKeys("ctrl+f"), + key.WithHelp("ctrl+f", "add attachment"), + ), + Cancel: key.NewBinding( + key.WithKeys("esc", "alt+esc"), + key.WithHelp("esc", "cancel"), + ), + Tab: key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("tab", "change focus"), + ), + Details: key.NewBinding( + key.WithKeys("ctrl+d"), + key.WithHelp("ctrl+d", "toggle details"), + ), + } +} + +// ChatModel represents the chat UI model. +type ChatModel struct { + app *app.App + com *common.Common + + keyMap ChatKeyMap +} + +// NewChatModel creates a new instance of ChatModel. +func NewChatModel(com *common.Common, app *app.App) *ChatModel { + return &ChatModel{ + app: app, + com: com, + keyMap: DefaultChatKeyMap(), + } +} + +// Init initializes the chat model. +func (m *ChatModel) Init() tea.Cmd { + return nil +} + +// Update handles incoming messages and updates the chat model state. +func (m *ChatModel) Update(msg tea.Msg) (*ChatModel, tea.Cmd) { + // Handle messages here + return m, nil +} + +// View renders the chat model's view. +func (m *ChatModel) View() string { + return "Chat Model View" +} + +// ShortHelp returns a brief help view for the chat model. +func (m *ChatModel) ShortHelp() []key.Binding { + return nil +} + +// FullHelp returns a detailed help view for the chat model. +func (m *ChatModel) FullHelp() [][]key.Binding { + return nil +} diff --git a/internal/ui/model/editor.go b/internal/ui/model/editor.go new file mode 100644 index 0000000000000000000000000000000000000000..384d9638a8ed34455df4ed50fe7c50227a530525 --- /dev/null +++ b/internal/ui/model/editor.go @@ -0,0 +1,201 @@ +package model + +import ( + "math/rand" + + "github.com/charmbracelet/bubbles/v2/key" + "github.com/charmbracelet/bubbles/v2/textarea" + tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/crush/internal/app" + "github.com/charmbracelet/crush/internal/ui/common" +) + +type EditorKeyMap struct { + AddFile key.Binding + SendMessage key.Binding + OpenEditor key.Binding + Newline key.Binding +} + +func DefaultEditorKeyMap() EditorKeyMap { + return EditorKeyMap{ + AddFile: key.NewBinding( + key.WithKeys("/"), + key.WithHelp("/", "add file"), + ), + SendMessage: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "send"), + ), + OpenEditor: key.NewBinding( + key.WithKeys("ctrl+o"), + key.WithHelp("ctrl+o", "open editor"), + ), + Newline: key.NewBinding( + key.WithKeys("shift+enter", "ctrl+j"), + // "ctrl+j" is a common keybinding for newline in many editors. If + // the terminal supports "shift+enter", we substitute the help text + // to reflect that. + key.WithHelp("ctrl+j", "newline"), + ), + } +} + +// EditorModel represents the editor UI model. +type EditorModel struct { + com *common.Common + app *app.App + + keyMap EditorKeyMap + textarea *textarea.Model + + readyPlaceholder string + workingPlaceholder string +} + +// NewEditorModel creates a new instance of EditorModel. +func NewEditorModel(com *common.Common, app *app.App) *EditorModel { + ta := textarea.New() + ta.SetStyles(com.Styles.TextArea) + ta.ShowLineNumbers = false + ta.CharLimit = -1 + ta.SetVirtualCursor(false) + ta.Focus() + e := &EditorModel{ + com: com, + app: app, + keyMap: DefaultEditorKeyMap(), + textarea: ta, + } + + e.setEditorPrompt() + e.randomizePlaceholders() + e.textarea.Placeholder = e.readyPlaceholder + + return e +} + +// Init initializes the editor model. +func (m *EditorModel) Init() tea.Cmd { + return nil +} + +// Update handles updates to the editor model. +func (m *EditorModel) Update(msg tea.Msg) (*EditorModel, tea.Cmd) { + var cmds []tea.Cmd + var cmd tea.Cmd + + m.textarea, cmd = m.textarea.Update(msg) + cmds = append(cmds, cmd) + + // Textarea placeholder logic + if m.app.AgentCoordinator != nil && m.app.AgentCoordinator.IsBusy() { + m.textarea.Placeholder = m.workingPlaceholder + } else { + m.textarea.Placeholder = m.readyPlaceholder + } + if m.app.Permissions.SkipRequests() { + m.textarea.Placeholder = "Yolo mode!" + } + + // TODO: Add attachments + + return m, tea.Batch(cmds...) +} + +// View renders the editor model. +func (m *EditorModel) View() string { + return m.textarea.View() +} + +// ShortHelp returns the short help view for the editor model. +func (m *EditorModel) ShortHelp() []key.Binding { + return nil +} + +// FullHelp returns the full help view for the editor model. +func (m *EditorModel) FullHelp() [][]key.Binding { + return nil +} + +// Cursor returns the relative cursor position of the editor. +func (m *EditorModel) Cursor() *tea.Cursor { + return m.textarea.Cursor() +} + +// Blur implements Container. +func (c *EditorModel) Blur() tea.Cmd { + c.textarea.Blur() + return nil +} + +// Focus implements Container. +func (c *EditorModel) Focus() tea.Cmd { + return c.textarea.Focus() +} + +// Focused returns whether the editor is focused. +func (c *EditorModel) Focused() bool { + return c.textarea.Focused() +} + +// SetSize sets the size of the editor. +func (m *EditorModel) SetSize(width, height int) { + m.textarea.SetWidth(width) + m.textarea.SetHeight(height) +} + +func (m *EditorModel) setEditorPrompt() { + if m.app.Permissions.SkipRequests() { + m.textarea.SetPromptFunc(4, m.yoloPromptFunc) + return + } + m.textarea.SetPromptFunc(4, m.normalPromptFunc) +} + +func (m *EditorModel) normalPromptFunc(info textarea.PromptInfo) string { + t := m.com.Styles + if info.LineNumber == 0 { + return " > " + } + if info.Focused { + return t.EditorPromptNormalFocused.Render() + } + return t.EditorPromptNormalBlurred.Render() +} + +func (m *EditorModel) yoloPromptFunc(info textarea.PromptInfo) string { + t := m.com.Styles + if info.LineNumber == 0 { + if info.Focused { + return t.EditorPromptYoloIconFocused.Render() + } else { + return t.EditorPromptYoloIconBlurred.Render() + } + } + if info.Focused { + return t.EditorPromptYoloDotsFocused.Render() + } + return t.EditorPromptYoloDotsBlurred.Render() +} + +var readyPlaceholders = [...]string{ + "Ready!", + "Ready...", + "Ready?", + "Ready for instructions", +} + +var workingPlaceholders = [...]string{ + "Working!", + "Working...", + "Brrrrr...", + "Prrrrrrrr...", + "Processing...", + "Thinking...", +} + +func (m *EditorModel) randomizePlaceholders() { + m.workingPlaceholder = workingPlaceholders[rand.Intn(len(workingPlaceholders))] + m.readyPlaceholder = readyPlaceholders[rand.Intn(len(readyPlaceholders))] +} diff --git a/internal/ui/model/keys.go b/internal/ui/model/keys.go index e0c1e0a6d96d0c3b624806146c80c5a668ef8aad..62b22544c5bd0964f6e1a978fa79be680a29366b 100644 --- a/internal/ui/model/keys.go +++ b/internal/ui/model/keys.go @@ -8,6 +8,7 @@ type KeyMap struct { Commands key.Binding Suspend key.Binding Sessions key.Binding + Tab key.Binding } func DefaultKeyMap() KeyMap { @@ -32,5 +33,9 @@ func DefaultKeyMap() KeyMap { key.WithKeys("ctrl+s"), key.WithHelp("ctrl+s", "sessions"), ), + Tab: key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("tab", "change focus"), + ), } } diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index fd5b8e16dc5cfb1bf7e8e281c36d4865b9b47d3f..20f1847cdf917eef2c9e1959dde067b3224da8cc 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -2,7 +2,9 @@ package model import ( "image" + "math/rand" + "github.com/charmbracelet/bubbles/v2/help" "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/app" @@ -15,19 +17,25 @@ import ( type uiState uint8 const ( - uiStateMain uiState = iota + uiChat uiState = iota + uiEdit ) type UI struct { app *app.App com *common.Common - width, height int - state uiState + state uiState + showFullHelp bool keyMap KeyMap + chat *ChatModel + editor *EditorModel dialog *dialog.Overlay + help help.Model + + layout layout } func New(com *common.Common, app *app.App) *UI { @@ -36,6 +44,8 @@ func New(com *common.Common, app *app.App) *UI { com: com, dialog: dialog.NewOverlay(), keyMap: DefaultKeyMap(), + editor: NewEditorModel(com, app), + help: help.New(), } } @@ -47,28 +57,35 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.WindowSizeMsg: - m.width = msg.Width - m.height = msg.Height + m.updateLayout(msg.Width, msg.Height) + m.editor.SetSize(m.layout.editor.Dx(), m.layout.editor.Dy()) case tea.KeyPressMsg: - switch m.state { - case uiStateMain: + if m.dialog.HasDialogs() { + m.updateDialogs(msg, &cmds) + } else { switch { + case key.Matches(msg, m.keyMap.Tab): + if m.state == uiChat { + m.state = uiEdit + cmds = append(cmds, m.editor.Focus()) + } else { + m.state = uiChat + cmds = append(cmds, m.editor.Blur()) + } + case key.Matches(msg, m.keyMap.Help): + m.showFullHelp = !m.showFullHelp + m.help.ShowAll = m.showFullHelp case key.Matches(msg, m.keyMap.Quit): - quitDialog := dialog.NewQuit(m.com) - if !m.dialog.ContainsDialog(quitDialog.ID()) { - m.dialog.AddDialog(quitDialog) + if !m.dialog.ContainsDialog(dialog.QuitDialogID) { + m.dialog.AddDialog(dialog.NewQuit(m.com)) return m, nil } + default: + m.updateFocused(msg, &cmds) } } } - updatedDialog, cmd := m.dialog.Update(msg) - m.dialog = updatedDialog - if cmd != nil { - cmds = append(cmds, cmd) - } - return m, tea.Batch(cmds...) } @@ -76,41 +93,63 @@ 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) layers := []*lipgloss.Layer{} - if dialogView := m.dialog.View(); dialogView != "" { - dialogWidth, dialogHeight := lipgloss.Width(dialogView), lipgloss.Height(dialogView) - dialogArea := centerRect(area, dialogWidth, dialogHeight) - layers = append(layers, - lipgloss.NewLayer(dialogView). - X(dialogArea.Min.X). - Y(dialogArea.Min.Y), - ) + // Determine the help key map based on focus + helpKeyMap := m.focusedKeyMap() + + // The screen areas we're working with + area := m.layout.area + chatRect := m.layout.chat + sideRect := m.layout.sidebar + editRect := m.layout.editor + helpRect := m.layout.help + + if m.dialog.HasDialogs() { + if dialogView := m.dialog.View(); dialogView != "" { + // If the dialog has its own help, use that instead + if len(m.dialog.FullHelp()) > 0 || len(m.dialog.ShortHelp()) > 0 { + helpKeyMap = m.dialog + } + + dialogWidth, dialogHeight := lipgloss.Width(dialogView), lipgloss.Height(dialogView) + dialogArea := common.CenterRect(area, dialogWidth, dialogHeight) + layers = append(layers, + lipgloss.NewLayer(dialogView). + X(dialogArea.Min.X). + Y(dialogArea.Min.Y). + Z(99), + ) + } } - mainRect, sideRect := uv.SplitHorizontal(area, uv.Fixed(area.Dx()-40)) - mainRect, footRect := uv.SplitVertical(mainRect, uv.Fixed(area.Dy()-7)) + if m.state == uiEdit && m.editor.Focused() { + cur := m.editor.Cursor() + cur.X++ // Adjust for app margins + cur.Y += editRect.Min.Y + v.Cursor = cur + } layers = append(layers, lipgloss.NewLayer( - lipgloss.NewStyle().Width(mainRect.Dx()). - Height(mainRect.Dy()). - Border(lipgloss.NormalBorder()). + lipgloss.NewStyle().Width(chatRect.Dx()). + Height(chatRect.Dy()). + Background(lipgloss.ANSIColor(rand.Intn(256))). Render(" Main View "), - ).X(mainRect.Min.X).Y(mainRect.Min.Y), + ).X(chatRect.Min.X).Y(chatRect.Min.Y), lipgloss.NewLayer( lipgloss.NewStyle().Width(sideRect.Dx()). Height(sideRect.Dy()). - Border(lipgloss.NormalBorder()). + Background(lipgloss.ANSIColor(rand.Intn(256))). Render(" Side View "), ).X(sideRect.Min.X).Y(sideRect.Min.Y), + lipgloss.NewLayer(m.editor.View()). + X(editRect.Min.X).Y(editRect.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), + lipgloss.NewStyle().Width(helpRect.Dx()). + Height(helpRect.Dy()). + Background(lipgloss.ANSIColor(rand.Intn(256))). + Render(m.help.View(helpKeyMap)), + ).X(helpRect.Min.X).Y(helpRect.Min.Y), ) v.Layer = lipgloss.NewCanvas(layers...) @@ -118,14 +157,96 @@ func (m *UI) View() tea.View { return v } -// centerRect returns a new [Rectangle] centered within the given area with the -// specified width and height. -func centerRect(area uv.Rectangle, width, height int) uv.Rectangle { - centerX := area.Min.X + area.Dx()/2 - centerY := area.Min.Y + area.Dy()/2 - minX := centerX - width/2 - minY := centerY - height/2 - maxX := minX + width - maxY := minY + height - return image.Rect(minX, minY, maxX, maxY) +func (m *UI) focusedKeyMap() help.KeyMap { + if m.state == uiChat { + return m.chat + } + return m.editor +} + +// updateDialogs updates the dialog overlay with the given message and appends +// any resulting commands to the cmds slice. +func (m *UI) updateDialogs(msg tea.KeyPressMsg, cmds *[]tea.Cmd) { + updatedDialog, cmd := m.dialog.Update(msg) + m.dialog = updatedDialog + if cmd != nil { + *cmds = append(*cmds, cmd) + } +} + +// updateFocused updates the focused model (chat or editor) with the given message +// and appends any resulting commands to the cmds slice. +func (m *UI) updateFocused(msg tea.KeyPressMsg, cmds *[]tea.Cmd) { + switch m.state { + case uiChat: + m.updateChat(msg, cmds) + case uiEdit: + m.updateEditor(msg, cmds) + } +} + +// updateChat updates the chat model with the given message and appends any +// resulting commands to the cmds slice. +func (m *UI) updateChat(msg tea.KeyPressMsg, cmds *[]tea.Cmd) { + updatedChat, cmd := m.chat.Update(msg) + m.chat = updatedChat + if cmd != nil { + *cmds = append(*cmds, cmd) + } +} + +// updateEditor updates the editor model with the given message and appends any +// resulting commands to the cmds slice. +func (m *UI) updateEditor(msg tea.KeyPressMsg, cmds *[]tea.Cmd) { + updatedEditor, cmd := m.editor.Update(msg) + m.editor = updatedEditor + if cmd != nil { + *cmds = append(*cmds, cmd) + } +} + +// updateLayout updates the layout based on the given terminal width and +// height given in cells. +func (m *UI) updateLayout(w, h int) { + // The screen area we're working with + area := image.Rect(1, 1, w-1, h-1) // -1 for margins + helpKeyMap := m.focusedKeyMap() + helpHeight := 1 + if m.showFullHelp { + helpHeight = max(1, len(helpKeyMap.FullHelp())) + } + + chatRect, sideRect := uv.SplitHorizontal(area, uv.Fixed(area.Dx()-40)) + chatRect, editRect := uv.SplitVertical(chatRect, uv.Fixed(area.Dy()-5-helpHeight)) + // Add 1 line margin bottom of mainRect + chatRect, _ = uv.SplitVertical(chatRect, uv.Fixed(chatRect.Dy()-1)) + editRect, helpRect := uv.SplitVertical(editRect, uv.Fixed(5)) + // Add 1 line margin bottom of footRect + editRect, _ = uv.SplitVertical(editRect, uv.Fixed(editRect.Dy()-1)) + + m.layout = layout{ + area: area, + chat: chatRect, + editor: editRect, + sidebar: sideRect, + help: helpRect, + } +} + +// layout defines the positioning of UI elements. +type layout struct { + // area is the overall available area. + area uv.Rectangle + + // chat is the area for the chat pane. + chat uv.Rectangle + + // editor is the area for the editor pane. + editor uv.Rectangle + + // sidebar is the area for the sidebar. + sidebar uv.Rectangle + + // help is the area for the help view. + help uv.Rectangle } diff --git a/internal/ui/styles/styles.go b/internal/ui/styles/styles.go index 8a1dcc71d0a3462305dea97b04291a0be6a122f6..297cadfe721432a40055b31c2817d7ecbe3237b8 100644 --- a/internal/ui/styles/styles.go +++ b/internal/ui/styles/styles.go @@ -5,6 +5,7 @@ import ( "github.com/charmbracelet/bubbles/v2/help" "github.com/charmbracelet/bubbles/v2/textarea" "github.com/charmbracelet/bubbles/v2/textinput" + tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/tui/exp/diffview" "github.com/charmbracelet/glamour/v2/ansi" "github.com/charmbracelet/lipgloss/v2" @@ -30,6 +31,11 @@ const ( BorderThick string = "▌" ) +const ( + defaultMargin = 2 + defaultListIndent = 2 +) + type Styles struct { WindowTooSmall lipgloss.Style @@ -96,20 +102,28 @@ type Styles struct { // Borders BorderFocus lipgloss.Style BorderBlur lipgloss.Style + + // Editor + EditorPromptNormalFocused lipgloss.Style + EditorPromptNormalBlurred lipgloss.Style + EditorPromptYoloIconFocused lipgloss.Style + EditorPromptYoloIconBlurred lipgloss.Style + EditorPromptYoloDotsFocused lipgloss.Style + EditorPromptYoloDotsBlurred lipgloss.Style } func DefaultStyles() Styles { var ( - // primary = charmtone.Charple + primary = charmtone.Charple secondary = charmtone.Dolly - // tertiary = charmtone.Bok + tertiary = charmtone.Bok // accent = charmtone.Zest // Backgrounds bgBase = charmtone.Pepper bgBaseLighter = charmtone.BBQ bgSubtle = charmtone.Charcoal - // bgOverlay = charmtone.Iron + bgOverlay = charmtone.Iron // Foregrounds fgBase = charmtone.Ash @@ -119,7 +133,7 @@ func DefaultStyles() Styles { // fgSelected = charmtone.Salt // Borders - // border = charmtone.Charcoal + border = charmtone.Charcoal borderFocus = charmtone.Charple // Status @@ -147,8 +161,334 @@ func DefaultStyles() Styles { // cherry = charmtone.Cherry ) + base := lipgloss.NewStyle().Foreground(fgBase) + s := Styles{} + s.TextInput = textinput.Styles{ + Focused: textinput.StyleState{ + Text: base, + Placeholder: base.Foreground(fgSubtle), + Prompt: base.Foreground(tertiary), + Suggestion: base.Foreground(fgSubtle), + }, + Blurred: textinput.StyleState{ + Text: base.Foreground(fgMuted), + Placeholder: base.Foreground(fgSubtle), + Prompt: base.Foreground(fgMuted), + Suggestion: base.Foreground(fgSubtle), + }, + Cursor: textinput.CursorStyle{ + Color: secondary, + Shape: tea.CursorBar, + Blink: true, + }, + } + + s.TextArea = textarea.Styles{ + Focused: textarea.StyleState{ + Base: base, + Text: base, + LineNumber: base.Foreground(fgSubtle), + CursorLine: base, + CursorLineNumber: base.Foreground(fgSubtle), + Placeholder: base.Foreground(fgSubtle), + Prompt: base.Foreground(tertiary), + }, + Blurred: textarea.StyleState{ + Base: base, + Text: base.Foreground(fgMuted), + LineNumber: base.Foreground(fgMuted), + CursorLine: base, + CursorLineNumber: base.Foreground(fgMuted), + Placeholder: base.Foreground(fgSubtle), + Prompt: base.Foreground(fgMuted), + }, + Cursor: textarea.CursorStyle{ + Color: secondary, + Shape: tea.CursorBar, + Blink: true, + }, + } + + s.Markdown = ansi.StyleConfig{ + Document: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + // BlockPrefix: "\n", + // BlockSuffix: "\n", + Color: stringPtr(charmtone.Smoke.Hex()), + }, + // Margin: uintPtr(defaultMargin), + }, + BlockQuote: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{}, + Indent: uintPtr(1), + IndentToken: stringPtr("│ "), + }, + List: ansi.StyleList{ + LevelIndent: defaultListIndent, + }, + Heading: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + BlockSuffix: "\n", + Color: stringPtr(charmtone.Malibu.Hex()), + Bold: boolPtr(true), + }, + }, + H1: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: " ", + Suffix: " ", + Color: stringPtr(charmtone.Zest.Hex()), + BackgroundColor: stringPtr(charmtone.Charple.Hex()), + Bold: boolPtr(true), + }, + }, + H2: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "## ", + }, + }, + H3: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "### ", + }, + }, + H4: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "#### ", + }, + }, + H5: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "##### ", + }, + }, + H6: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "###### ", + Color: stringPtr(charmtone.Guac.Hex()), + Bold: boolPtr(false), + }, + }, + Strikethrough: ansi.StylePrimitive{ + CrossedOut: boolPtr(true), + }, + Emph: ansi.StylePrimitive{ + Italic: boolPtr(true), + }, + Strong: ansi.StylePrimitive{ + Bold: boolPtr(true), + }, + HorizontalRule: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Charcoal.Hex()), + Format: "\n--------\n", + }, + Item: ansi.StylePrimitive{ + BlockPrefix: "• ", + }, + Enumeration: ansi.StylePrimitive{ + BlockPrefix: ". ", + }, + Task: ansi.StyleTask{ + StylePrimitive: ansi.StylePrimitive{}, + Ticked: "[✓] ", + Unticked: "[ ] ", + }, + Link: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Zinc.Hex()), + Underline: boolPtr(true), + }, + LinkText: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Guac.Hex()), + Bold: boolPtr(true), + }, + Image: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Cheeky.Hex()), + Underline: boolPtr(true), + }, + ImageText: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Squid.Hex()), + Format: "Image: {{.text}} →", + }, + Code: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: " ", + Suffix: " ", + Color: stringPtr(charmtone.Coral.Hex()), + BackgroundColor: stringPtr(charmtone.Charcoal.Hex()), + }, + }, + CodeBlock: ansi.StyleCodeBlock{ + StyleBlock: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Charcoal.Hex()), + }, + Margin: uintPtr(defaultMargin), + }, + Chroma: &ansi.Chroma{ + Text: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Smoke.Hex()), + }, + Error: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Butter.Hex()), + BackgroundColor: stringPtr(charmtone.Sriracha.Hex()), + }, + Comment: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Oyster.Hex()), + }, + CommentPreproc: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Bengal.Hex()), + }, + Keyword: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Malibu.Hex()), + }, + KeywordReserved: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Pony.Hex()), + }, + KeywordNamespace: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Pony.Hex()), + }, + KeywordType: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Guppy.Hex()), + }, + Operator: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Salmon.Hex()), + }, + Punctuation: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Zest.Hex()), + }, + Name: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Smoke.Hex()), + }, + NameBuiltin: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Cheeky.Hex()), + }, + NameTag: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Mauve.Hex()), + }, + NameAttribute: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Hazy.Hex()), + }, + NameClass: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Salt.Hex()), + Underline: boolPtr(true), + Bold: boolPtr(true), + }, + NameDecorator: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Citron.Hex()), + }, + NameFunction: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Guac.Hex()), + }, + LiteralNumber: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Julep.Hex()), + }, + LiteralString: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Cumin.Hex()), + }, + LiteralStringEscape: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Bok.Hex()), + }, + GenericDeleted: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Coral.Hex()), + }, + GenericEmph: ansi.StylePrimitive{ + Italic: boolPtr(true), + }, + GenericInserted: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Guac.Hex()), + }, + GenericStrong: ansi.StylePrimitive{ + Bold: boolPtr(true), + }, + GenericSubheading: ansi.StylePrimitive{ + Color: stringPtr(charmtone.Squid.Hex()), + }, + Background: ansi.StylePrimitive{ + BackgroundColor: stringPtr(charmtone.Charcoal.Hex()), + }, + }, + }, + Table: ansi.StyleTable{ + StyleBlock: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{}, + }, + }, + DefinitionDescription: ansi.StylePrimitive{ + BlockPrefix: "\n ", + }, + } + + s.Help = help.Styles{ + ShortKey: base.Foreground(fgMuted), + ShortDesc: base.Foreground(fgSubtle), + ShortSeparator: base.Foreground(border), + Ellipsis: base.Foreground(border), + FullKey: base.Foreground(fgMuted), + FullDesc: base.Foreground(fgSubtle), + FullSeparator: base.Foreground(border), + } + + s.Diff = diffview.Style{ + DividerLine: diffview.LineStyle{ + LineNumber: lipgloss.NewStyle(). + Foreground(fgHalfMuted). + Background(bgBaseLighter), + Code: lipgloss.NewStyle(). + Foreground(fgHalfMuted). + Background(bgBaseLighter), + }, + MissingLine: diffview.LineStyle{ + LineNumber: lipgloss.NewStyle(). + Background(bgBaseLighter), + Code: lipgloss.NewStyle(). + Background(bgBaseLighter), + }, + EqualLine: diffview.LineStyle{ + LineNumber: lipgloss.NewStyle(). + Foreground(fgMuted). + Background(bgBase), + Code: lipgloss.NewStyle(). + Foreground(fgMuted). + Background(bgBase), + }, + InsertLine: diffview.LineStyle{ + LineNumber: lipgloss.NewStyle(). + Foreground(lipgloss.Color("#629657")). + Background(lipgloss.Color("#2b322a")), + Symbol: lipgloss.NewStyle(). + Foreground(lipgloss.Color("#629657")). + Background(lipgloss.Color("#323931")), + Code: lipgloss.NewStyle(). + Background(lipgloss.Color("#323931")), + }, + DeleteLine: diffview.LineStyle{ + LineNumber: lipgloss.NewStyle(). + Foreground(lipgloss.Color("#a45c59")). + Background(lipgloss.Color("#312929")), + Symbol: lipgloss.NewStyle(). + Foreground(lipgloss.Color("#a45c59")). + Background(lipgloss.Color("#383030")), + Code: lipgloss.NewStyle(). + Background(lipgloss.Color("#383030")), + }, + } + + s.FilePicker = filepicker.Styles{ + DisabledCursor: base.Foreground(fgMuted), + Cursor: base.Foreground(fgBase), + Symlink: base.Foreground(fgSubtle), + Directory: base.Foreground(primary), + File: base.Foreground(fgBase), + DisabledFile: base.Foreground(fgMuted), + DisabledSelected: base.Background(bgOverlay).Foreground(fgMuted), + Permission: base.Foreground(fgMuted), + Selected: base.Background(primary).Foreground(fgBase), + FileSize: base.Foreground(fgMuted), + EmptyDirectory: base.Foreground(fgMuted).PaddingLeft(2).SetString("Empty directory"), + } + // borders s.FocusedMessageBorder = lipgloss.Border{Left: BorderThick} @@ -190,5 +530,18 @@ func DefaultStyles() Styles { // Borders s.BorderFocus = lipgloss.NewStyle().BorderForeground(borderFocus).Border(lipgloss.RoundedBorder()).Padding(1, 2) + // Editor + s.EditorPromptNormalFocused = lipgloss.NewStyle().Foreground(greenDark).SetString("::: ") + s.EditorPromptNormalBlurred = s.EditorPromptNormalFocused.Foreground(fgMuted) + s.EditorPromptYoloIconFocused = lipgloss.NewStyle().Foreground(charmtone.Oyster).Background(charmtone.Citron).Bold(true).SetString(" ! ") + s.EditorPromptYoloIconBlurred = s.EditorPromptYoloIconFocused.Foreground(charmtone.Pepper).Background(charmtone.Squid) + s.EditorPromptYoloDotsFocused = lipgloss.NewStyle().Foreground(charmtone.Zest).SetString(":::") + s.EditorPromptYoloDotsBlurred = s.EditorPromptYoloDotsFocused.Foreground(charmtone.Squid) + return s } + +// Helper functions for style pointers +func boolPtr(b bool) *bool { return &b } +func stringPtr(s string) *string { return &s } +func uintPtr(u uint) *uint { return &u }