@@ -0,0 +1,106 @@
+package model
+
+import (
+ "time"
+
+ "charm.land/bubbles/v2/help"
+ tea "charm.land/bubbletea/v2"
+ "charm.land/lipgloss/v2"
+ "github.com/charmbracelet/crush/internal/ui/common"
+ "github.com/charmbracelet/crush/internal/uiutil"
+ uv "github.com/charmbracelet/ultraviolet"
+ "github.com/charmbracelet/x/ansi"
+)
+
+// DefaultStatusTTL is the default time-to-live for status messages.
+const DefaultStatusTTL = 5 * time.Second
+
+// Status is the status bar and help model.
+type Status struct {
+ com *common.Common
+ help help.Model
+ helpKm help.KeyMap
+ msg uiutil.InfoMsg
+}
+
+// NewStatus creates a new status bar and help model.
+func NewStatus(com *common.Common, km help.KeyMap) *Status {
+ s := new(Status)
+ s.com = com
+ s.help = help.New()
+ s.help.Styles = com.Styles.Help
+ s.helpKm = km
+ return s
+}
+
+// SetInfoMsg sets the status info message.
+func (s *Status) SetInfoMsg(msg uiutil.InfoMsg) {
+ s.msg = msg
+}
+
+// ClearInfoMsg clears the status info message.
+func (s *Status) ClearInfoMsg() {
+ s.msg = uiutil.InfoMsg{}
+}
+
+// SetWidth sets the width of the status bar and help view.
+func (s *Status) SetWidth(width int) {
+ s.help.SetWidth(width)
+}
+
+// ShowingAll returns whether the full help view is shown.
+func (s *Status) ShowingAll() bool {
+ return s.help.ShowAll
+}
+
+// ToggleHelp toggles the full help view.
+func (s *Status) ToggleHelp() {
+ s.help.ShowAll = !s.help.ShowAll
+}
+
+// Draw draws the status bar onto the screen.
+func (s *Status) Draw(scr uv.Screen, area uv.Rectangle) {
+ helpView := s.com.Styles.Status.Help.Render(s.help.View(s.helpKm))
+ uv.NewStyledString(helpView).Draw(scr, area)
+
+ // Render notifications
+ if s.msg.IsEmpty() {
+ return
+ }
+
+ var indStyle lipgloss.Style
+ var msgStyle lipgloss.Style
+ switch s.msg.Type {
+ case uiutil.InfoTypeError:
+ indStyle = s.com.Styles.Status.ErrorIndicator
+ msgStyle = s.com.Styles.Status.ErrorMessage
+ case uiutil.InfoTypeWarn:
+ indStyle = s.com.Styles.Status.WarnIndicator
+ msgStyle = s.com.Styles.Status.WarnMessage
+ case uiutil.InfoTypeUpdate:
+ indStyle = s.com.Styles.Status.UpdateIndicator
+ msgStyle = s.com.Styles.Status.UpdateMessage
+ case uiutil.InfoTypeInfo:
+ indStyle = s.com.Styles.Status.InfoIndicator
+ msgStyle = s.com.Styles.Status.InfoMessage
+ case uiutil.InfoTypeSuccess:
+ indStyle = s.com.Styles.Status.SuccessIndicator
+ msgStyle = s.com.Styles.Status.SuccessMessage
+ }
+
+ ind := indStyle.String()
+ messageWidth := area.Dx() - lipgloss.Width(ind)
+ msg := ansi.Truncate(s.msg.Msg, messageWidth, "…")
+ info := msgStyle.Width(messageWidth).Render(msg)
+
+ // Draw the info message over the help view
+ uv.NewStyledString(ind+info).Draw(scr, area)
+}
+
+// clearInfoMsgCmd returns a command that clears the info message after the
+// given TTL.
+func clearInfoMsgCmd(ttl time.Duration) tea.Cmd {
+ return tea.Tick(ttl, func(time.Time) tea.Msg {
+ return uiutil.ClearStatusMsg{}
+ })
+}
@@ -82,7 +82,7 @@ type UI struct {
keyenh tea.KeyboardEnhancementsMsg
dialog *dialog.Overlay
- help help.Model
+ status *Status
// header is the last cached header logo
header string
@@ -137,13 +137,14 @@ func New(com *common.Common) *UI {
com: com,
dialog: dialog.NewOverlay(),
keyMap: DefaultKeyMap(),
- help: help.New(),
focus: uiFocusNone,
state: uiConfigure,
textarea: ta,
chat: ch,
}
+ status := NewStatus(com, ui)
+
// set onboarding state defaults
ui.onboarding.yesInitializeSelected = true
@@ -162,7 +163,7 @@ func New(com *common.Common) *UI {
ui.setEditorPrompt(false)
ui.randomizePlaceholders()
ui.textarea.Placeholder = ui.readyPlaceholder
- ui.help.Styles = com.Styles.Help
+ ui.status = status
return ui
}
@@ -338,6 +339,15 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case openEditorMsg:
m.textarea.SetValue(msg.Text)
m.textarea.MoveToEnd()
+ case uiutil.InfoMsg:
+ m.status.SetInfoMsg(msg)
+ ttl := msg.TTL
+ if ttl <= 0 {
+ ttl = DefaultStatusTTL
+ }
+ cmds = append(cmds, clearInfoMsgCmd(ttl))
+ case uiutil.ClearStatusMsg:
+ m.status.ClearInfoMsg()
}
// This logic gets triggered on any message type, but should it?
@@ -480,7 +490,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
handleGlobalKeys := func(msg tea.KeyPressMsg) bool {
switch {
case key.Matches(msg, m.keyMap.Help):
- m.help.ShowAll = !m.help.ShowAll
+ m.status.ToggleHelp()
m.updateLayoutAndSize()
return true
case key.Matches(msg, m.keyMap.Commands):
@@ -569,7 +579,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
cmds = append(cmds, uiutil.ReportError(err))
}
case dialog.ToggleHelpMsg:
- m.help.ShowAll = !m.help.ShowAll
+ m.status.ToggleHelp()
m.dialog.CloseDialog(dialog.CommandsID)
case dialog.QuitMsg:
cmds = append(cmds, tea.Quit)
@@ -815,9 +825,8 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) {
editor.Draw(scr, layout.editor)
}
- // Add help layer
- help := uv.NewStyledString(m.help.View(m))
- help.Draw(scr, layout.help)
+ // Add status and help layer
+ m.status.Draw(scr, layout.status)
// Debugging rendering (visually see when the tui rerenders)
if os.Getenv("CRUSH_UI_DEBUG") == "true" {
@@ -1077,8 +1086,8 @@ func (m *UI) updateLayoutAndSize() {
// updateSize updates the sizes of UI components based on the current layout.
func (m *UI) updateSize() {
- // Set help width
- m.help.SetWidth(m.layout.help.Dx())
+ // Set status width
+ m.status.SetWidth(m.layout.status.Dx())
m.chat.SetSize(m.layout.main.Dx(), m.layout.main.Dy())
m.textarea.SetWidth(m.layout.editor.Dx())
@@ -1115,18 +1124,16 @@ func (m *UI) generateLayout(w, h int) layout {
headerHeight := 4
var helpKeyMap help.KeyMap = m
- if m.help.ShowAll {
+ if m.status.ShowingAll() {
for _, row := range helpKeyMap.FullHelp() {
helpHeight = max(helpHeight, len(row))
}
}
// Add app margins
- appRect := area
+ appRect, helpRect := uv.SplitVertical(area, uv.Fixed(area.Dy()-helpHeight))
appRect.Min.X += 1
- appRect.Min.Y += 1
appRect.Max.X -= 1
- appRect.Max.Y -= 1
if slices.Contains([]uiState{uiConfigure, uiInitialize, uiLanding}, m.state) {
// extra padding on left and right for these states
@@ -1134,11 +1141,9 @@ func (m *UI) generateLayout(w, h int) layout {
appRect.Max.X -= 1
}
- appRect, helpRect := uv.SplitVertical(appRect, uv.Fixed(appRect.Dy()-helpHeight))
-
layout := layout{
- area: area,
- help: helpRect,
+ area: area,
+ status: helpRect,
}
// Handle different app states
@@ -1242,8 +1247,8 @@ type layout struct {
// sidebar is the area for the sidebar.
sidebar uv.Rectangle
- // help is the area for the help view.
- help uv.Rectangle
+ // status is the area for the status view.
+ status uv.Rectangle
}
func (m *UI) openEditor(value string) tea.Cmd {
@@ -303,6 +303,23 @@ type Styles struct {
Commands struct{}
}
+
+ // Status bar and help
+ Status struct {
+ Help lipgloss.Style
+
+ ErrorIndicator lipgloss.Style
+ WarnIndicator lipgloss.Style
+ InfoIndicator lipgloss.Style
+ UpdateIndicator lipgloss.Style
+ SuccessIndicator lipgloss.Style
+
+ ErrorMessage lipgloss.Style
+ WarnMessage lipgloss.Style
+ InfoMessage lipgloss.Style
+ UpdateMessage lipgloss.Style
+ SuccessMessage lipgloss.Style
+ }
}
// ChromaTheme converts the current markdown chroma styles to a chroma
@@ -1110,6 +1127,18 @@ func DefaultStyles() Styles {
s.Dialog.List = base.Margin(0, 0, 1, 0)
+ s.Status.Help = lipgloss.NewStyle().Padding(0, 1)
+ s.Status.SuccessIndicator = base.Foreground(bgSubtle).Background(green).Padding(0, 1).Bold(true).SetString("OKAY!")
+ s.Status.InfoIndicator = s.Status.SuccessIndicator
+ s.Status.UpdateIndicator = s.Status.SuccessIndicator.SetString("HEY!")
+ s.Status.WarnIndicator = s.Status.SuccessIndicator.Foreground(bgOverlay).Background(yellow).SetString("WARNING")
+ s.Status.ErrorIndicator = s.Status.SuccessIndicator.Foreground(bgBase).Background(red).SetString("ERROR")
+ s.Status.SuccessMessage = base.Foreground(bgSubtle).Background(greenDark).Padding(0, 1)
+ s.Status.InfoMessage = s.Status.SuccessMessage
+ s.Status.UpdateMessage = s.Status.SuccessMessage
+ s.Status.WarnMessage = s.Status.SuccessMessage.Foreground(bgOverlay).Background(warning)
+ s.Status.ErrorMessage = s.Status.SuccessMessage.Foreground(white).Background(redDark)
+
return s
}