Detailed changes
@@ -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)
+}
@@ -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
+}
@@ -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 {
@@ -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].
@@ -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
+}
@@ -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))]
+}
@@ -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"),
+ ),
}
}
@@ -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
}
@@ -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 }