Detailed changes
@@ -157,9 +157,6 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
var cmds []tea.Cmd
switch msg := msg.(type) {
- case tea.KeyboardEnhancementsMsg:
- m.keyMap.keyboard = msg
- return m, nil
case filepicker.FilePickedMsg:
if len(m.attachments) >= maxAttachments {
return m, util.ReportError(fmt.Errorf("cannot add more than %d images", maxAttachments))
@@ -2,7 +2,6 @@ package editor
import (
"github.com/charmbracelet/bubbles/v2/key"
- tea "github.com/charmbracelet/bubbletea/v2"
)
type EditorKeyMap struct {
@@ -10,8 +9,6 @@ type EditorKeyMap struct {
SendMessage key.Binding
OpenEditor key.Binding
Newline key.Binding
-
- keyboard tea.KeyboardEnhancementsMsg
}
func DefaultEditorKeyMap() EditorKeyMap {
@@ -40,15 +37,11 @@ func DefaultEditorKeyMap() EditorKeyMap {
// KeyBindings implements layout.KeyMapProvider
func (k EditorKeyMap) KeyBindings() []key.Binding {
- newline := k.Newline
- if k.keyboard.SupportsKeyDisambiguation() {
- newline.SetHelp("shift+enter", newline.Help().Desc)
- }
return []key.Binding{
k.AddFile,
k.SendMessage,
k.OpenEditor,
- newline,
+ k.Newline,
}
}
@@ -32,6 +32,9 @@ type Splash interface {
SetOnboarding(bool)
// SetProjectInit controls whether the splash shows project initialization prompt
SetProjectInit(bool)
+
+ // Showing API key input
+ IsShowingAPIKey() bool
}
const (
@@ -126,9 +129,11 @@ func (s *splashCmp) Init() tea.Cmd {
// SetSize implements SplashPage.
func (s *splashCmp) SetSize(width int, height int) tea.Cmd {
- s.width = width
s.height = height
- s.logoRendered = s.logoBlock()
+ if width != s.width {
+ s.width = width
+ s.logoRendered = s.logoBlock()
+ }
listHeigh := min(40, height-(SplashScreenPaddingY*2)-lipgloss.Height(s.logoRendered)-2) // -1 for the title
listWidth := min(60, width-(SplashScreenPaddingX*2))
@@ -626,3 +631,7 @@ func (s *splashCmp) mcpBlock() string {
),
)
}
+
+func (s *splashCmp) IsShowingAPIKey() bool {
+ return s.needsAPIKey
+}
@@ -5,12 +5,40 @@ import (
"strings"
"github.com/alecthomas/chroma/v2"
+ "github.com/charmbracelet/bubbles/v2/help"
+ "github.com/charmbracelet/bubbles/v2/key"
"github.com/charmbracelet/crush/internal/tui/exp/diffview"
"github.com/charmbracelet/crush/internal/tui/styles"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/x/ansi"
)
+type KeyMapHelp interface {
+ Help() help.KeyMap
+}
+
+type simpleHelp struct {
+ shortList []key.Binding
+ fullList [][]key.Binding
+}
+
+func NewSimpleHelp(shortList []key.Binding, fullList [][]key.Binding) help.KeyMap {
+ return &simpleHelp{
+ shortList: shortList,
+ fullList: fullList,
+ }
+}
+
+// FullHelp implements help.KeyMap.
+func (s *simpleHelp) FullHelp() [][]key.Binding {
+ return s.fullList
+}
+
+// ShortHelp implements help.KeyMap.
+func (s *simpleHelp) ShortHelp() []key.Binding {
+ return s.shortList
+}
+
func Section(text string, width int) string {
t := styles.CurrentTheme()
char := "─"
@@ -1,263 +0,0 @@
-package layout
-
-import (
- "github.com/charmbracelet/bubbles/v2/key"
- tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/crush/internal/tui/styles"
- "github.com/charmbracelet/crush/internal/tui/util"
- "github.com/charmbracelet/lipgloss/v2"
-)
-
-type Container interface {
- util.Model
- Sizeable
- Help
- Positional
- Focusable
-}
-type container struct {
- width int
- height int
- isFocused bool
-
- x, y int
-
- content util.Model
-
- // Style options
- paddingTop int
- paddingRight int
- paddingBottom int
- paddingLeft int
-
- borderTop bool
- borderRight bool
- borderBottom bool
- borderLeft bool
- borderStyle lipgloss.Border
-}
-
-type ContainerOption func(*container)
-
-func NewContainer(content util.Model, options ...ContainerOption) Container {
- c := &container{
- content: content,
- borderStyle: lipgloss.NormalBorder(),
- }
-
- for _, option := range options {
- option(c)
- }
-
- return c
-}
-
-func (c *container) Init() tea.Cmd {
- return c.content.Init()
-}
-
-func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg := msg.(type) {
- case tea.KeyPressMsg:
- if c.IsFocused() {
- u, cmd := c.content.Update(msg)
- c.content = u.(util.Model)
- return c, cmd
- }
- return c, nil
- default:
- u, cmd := c.content.Update(msg)
- c.content = u.(util.Model)
- return c, cmd
- }
-}
-
-func (c *container) Cursor() *tea.Cursor {
- if cursor, ok := c.content.(util.Cursor); ok {
- return cursor.Cursor()
- }
- return nil
-}
-
-func (c *container) View() string {
- t := styles.CurrentTheme()
- width := c.width
- height := c.height
-
- style := t.S().Base
-
- // Apply border if any side is enabled
- if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
- // Adjust width and height for borders
- if c.borderTop {
- height--
- }
- if c.borderBottom {
- height--
- }
- if c.borderLeft {
- width--
- }
- if c.borderRight {
- width--
- }
- style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
- style = style.BorderBackground(t.BgBase).BorderForeground(t.Border)
- }
- style = style.
- Width(width).
- Height(height).
- PaddingTop(c.paddingTop).
- PaddingRight(c.paddingRight).
- PaddingBottom(c.paddingBottom).
- PaddingLeft(c.paddingLeft)
-
- contentView := c.content.View()
- return style.Render(contentView)
-}
-
-func (c *container) SetSize(width, height int) tea.Cmd {
- c.width = width
- c.height = height
-
- // If the content implements Sizeable, adjust its size to account for padding and borders
- if sizeable, ok := c.content.(Sizeable); ok {
- // Calculate horizontal space taken by padding and borders
- horizontalSpace := c.paddingLeft + c.paddingRight
- if c.borderLeft {
- horizontalSpace++
- }
- if c.borderRight {
- horizontalSpace++
- }
-
- // Calculate vertical space taken by padding and borders
- verticalSpace := c.paddingTop + c.paddingBottom
- if c.borderTop {
- verticalSpace++
- }
- if c.borderBottom {
- verticalSpace++
- }
-
- // Set content size with adjusted dimensions
- contentWidth := max(0, width-horizontalSpace)
- contentHeight := max(0, height-verticalSpace)
- return sizeable.SetSize(contentWidth, contentHeight)
- }
- return nil
-}
-
-func (c *container) GetSize() (int, int) {
- return c.width, c.height
-}
-
-func (c *container) SetPosition(x, y int) tea.Cmd {
- c.x = x
- c.y = y
- if positionable, ok := c.content.(Positional); ok {
- return positionable.SetPosition(x, y)
- }
- return nil
-}
-
-func (c *container) Bindings() []key.Binding {
- if b, ok := c.content.(Help); ok {
- return b.Bindings()
- }
- return nil
-}
-
-// Blur implements Container.
-func (c *container) Blur() tea.Cmd {
- c.isFocused = false
- if focusable, ok := c.content.(Focusable); ok {
- return focusable.Blur()
- }
- return nil
-}
-
-// Focus implements Container.
-func (c *container) Focus() tea.Cmd {
- c.isFocused = true
- if focusable, ok := c.content.(Focusable); ok {
- return focusable.Focus()
- }
- return nil
-}
-
-// IsFocused implements Container.
-func (c *container) IsFocused() bool {
- isFocused := c.isFocused
- if focusable, ok := c.content.(Focusable); ok {
- isFocused = isFocused || focusable.IsFocused()
- }
- return isFocused
-}
-
-// Padding options
-func WithPadding(top, right, bottom, left int) ContainerOption {
- return func(c *container) {
- c.paddingTop = top
- c.paddingRight = right
- c.paddingBottom = bottom
- c.paddingLeft = left
- }
-}
-
-func WithPaddingAll(padding int) ContainerOption {
- return WithPadding(padding, padding, padding, padding)
-}
-
-func WithPaddingHorizontal(padding int) ContainerOption {
- return func(c *container) {
- c.paddingLeft = padding
- c.paddingRight = padding
- }
-}
-
-func WithPaddingVertical(padding int) ContainerOption {
- return func(c *container) {
- c.paddingTop = padding
- c.paddingBottom = padding
- }
-}
-
-func WithBorder(top, right, bottom, left bool) ContainerOption {
- return func(c *container) {
- c.borderTop = top
- c.borderRight = right
- c.borderBottom = bottom
- c.borderLeft = left
- }
-}
-
-func WithBorderAll() ContainerOption {
- return WithBorder(true, true, true, true)
-}
-
-func WithBorderHorizontal() ContainerOption {
- return WithBorder(true, false, true, false)
-}
-
-func WithBorderVertical() ContainerOption {
- return WithBorder(false, true, false, true)
-}
-
-func WithBorderStyle(style lipgloss.Border) ContainerOption {
- return func(c *container) {
- c.borderStyle = style
- }
-}
-
-func WithRoundedBorder() ContainerOption {
- return WithBorderStyle(lipgloss.RoundedBorder())
-}
-
-func WithThickBorder() ContainerOption {
- return WithBorderStyle(lipgloss.ThickBorder())
-}
-
-func WithDoubleBorder() ContainerOption {
- return WithBorderStyle(lipgloss.DoubleBorder())
-}
@@ -5,6 +5,8 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
)
+// TODO: move this to core
+
type Focusable interface {
Focus() tea.Cmd
Blur() tea.Cmd
@@ -23,8 +25,3 @@ type Help interface {
type Positional interface {
SetPosition(x, y int) tea.Cmd
}
-
-// KeyMapProvider defines an interface for types that can provide their key bindings as a slice
-type KeyMapProvider interface {
- KeyBindings() []key.Binding
-}
@@ -1,375 +0,0 @@
-package layout
-
-import (
- "github.com/charmbracelet/bubbles/v2/key"
- tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/crush/internal/tui/styles"
- "github.com/charmbracelet/crush/internal/tui/util"
- "github.com/charmbracelet/lipgloss/v2"
-)
-
-type LayoutPanel string
-
-const (
- LeftPanel LayoutPanel = "left"
- RightPanel LayoutPanel = "right"
- BottomPanel LayoutPanel = "bottom"
-)
-
-type SplitPaneLayout interface {
- util.Model
- Sizeable
- Help
- SetLeftPanel(panel Container) tea.Cmd
- SetRightPanel(panel Container) tea.Cmd
- SetBottomPanel(panel Container) tea.Cmd
-
- ClearLeftPanel() tea.Cmd
- ClearRightPanel() tea.Cmd
- ClearBottomPanel() tea.Cmd
-
- FocusPanel(panel LayoutPanel) tea.Cmd
- SetOffset(x, y int)
-}
-
-type splitPaneLayout struct {
- width int
- height int
- xOffset int
- yOffset int
-
- ratio float64
- verticalRatio float64
-
- rightPanel Container
- leftPanel Container
- bottomPanel Container
-
- fixedBottomHeight int // Fixed height for the bottom panel, if any
- fixedRightWidth int // Fixed width for the right panel, if any
-}
-
-type SplitPaneOption func(*splitPaneLayout)
-
-func (s *splitPaneLayout) Init() tea.Cmd {
- var cmds []tea.Cmd
-
- if s.leftPanel != nil {
- cmds = append(cmds, s.leftPanel.Init())
- }
-
- if s.rightPanel != nil {
- cmds = append(cmds, s.rightPanel.Init())
- }
-
- if s.bottomPanel != nil {
- cmds = append(cmds, s.bottomPanel.Init())
- }
-
- return tea.Batch(cmds...)
-}
-
-func (s *splitPaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmds []tea.Cmd
- switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- return s, s.SetSize(msg.Width, msg.Height)
- }
-
- if s.rightPanel != nil {
- u, cmd := s.rightPanel.Update(msg)
- s.rightPanel = u.(Container)
- if cmd != nil {
- cmds = append(cmds, cmd)
- }
- }
-
- if s.leftPanel != nil {
- u, cmd := s.leftPanel.Update(msg)
- s.leftPanel = u.(Container)
- if cmd != nil {
- cmds = append(cmds, cmd)
- }
- }
-
- if s.bottomPanel != nil {
- u, cmd := s.bottomPanel.Update(msg)
- s.bottomPanel = u.(Container)
- if cmd != nil {
- cmds = append(cmds, cmd)
- }
- }
-
- return s, tea.Batch(cmds...)
-}
-
-func (s *splitPaneLayout) Cursor() *tea.Cursor {
- if s.bottomPanel != nil {
- if c, ok := s.bottomPanel.(util.Cursor); ok {
- return c.Cursor()
- }
- } else if s.rightPanel != nil {
- if c, ok := s.rightPanel.(util.Cursor); ok {
- return c.Cursor()
- }
- } else if s.leftPanel != nil {
- if c, ok := s.leftPanel.(util.Cursor); ok {
- return c.Cursor()
- }
- }
- return nil
-}
-
-func (s *splitPaneLayout) View() string {
- var topSection string
-
- if s.leftPanel != nil && s.rightPanel != nil {
- leftView := s.leftPanel.View()
- rightView := s.rightPanel.View()
- topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView, rightView)
- } else if s.leftPanel != nil {
- topSection = s.leftPanel.View()
- } else if s.rightPanel != nil {
- topSection = s.rightPanel.View()
- } else {
- topSection = ""
- }
-
- var finalView string
-
- if s.bottomPanel != nil && topSection != "" {
- bottomView := s.bottomPanel.View()
- finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView)
- } else if s.bottomPanel != nil {
- finalView = s.bottomPanel.View()
- } else {
- finalView = topSection
- }
-
- t := styles.CurrentTheme()
-
- style := t.S().Base.
- Width(s.width).
- Height(s.height)
-
- return style.Render(finalView)
-}
-
-func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
- s.width = width
- s.height = height
- var topHeight, bottomHeight int
- var cmds []tea.Cmd
- if s.bottomPanel != nil {
- if s.fixedBottomHeight > 0 {
- bottomHeight = s.fixedBottomHeight
- topHeight = height - bottomHeight
- } else {
- topHeight = int(float64(height) * s.verticalRatio)
- bottomHeight = height - topHeight
- if bottomHeight <= 0 {
- bottomHeight = 2
- topHeight = height - bottomHeight
- }
- }
- } else {
- topHeight = height
- bottomHeight = 0
- }
-
- var leftWidth, rightWidth int
- if s.leftPanel != nil && s.rightPanel != nil {
- if s.fixedRightWidth > 0 {
- rightWidth = s.fixedRightWidth
- leftWidth = width - rightWidth
- } else {
- leftWidth = int(float64(width) * s.ratio)
- rightWidth = width - leftWidth
- if rightWidth <= 0 {
- rightWidth = 2
- leftWidth = width - rightWidth
- }
- }
- } else if s.leftPanel != nil {
- leftWidth = width
- rightWidth = 0
- } else if s.rightPanel != nil {
- leftWidth = 0
- rightWidth = width
- }
-
- if s.leftPanel != nil {
- cmd := s.leftPanel.SetSize(leftWidth, topHeight)
- cmds = append(cmds, cmd)
- if positional, ok := s.leftPanel.(Positional); ok {
- cmds = append(cmds, positional.SetPosition(s.xOffset, s.yOffset))
- }
- }
-
- if s.rightPanel != nil {
- cmd := s.rightPanel.SetSize(rightWidth, topHeight)
- cmds = append(cmds, cmd)
- if positional, ok := s.rightPanel.(Positional); ok {
- cmds = append(cmds, positional.SetPosition(s.xOffset+leftWidth, s.yOffset))
- }
- }
-
- if s.bottomPanel != nil {
- cmd := s.bottomPanel.SetSize(width, bottomHeight)
- cmds = append(cmds, cmd)
- if positional, ok := s.bottomPanel.(Positional); ok {
- cmds = append(cmds, positional.SetPosition(s.xOffset, s.yOffset+topHeight))
- }
- }
- return tea.Batch(cmds...)
-}
-
-func (s *splitPaneLayout) GetSize() (int, int) {
- return s.width, s.height
-}
-
-func (s *splitPaneLayout) SetLeftPanel(panel Container) tea.Cmd {
- s.leftPanel = panel
- if s.width > 0 && s.height > 0 {
- return s.SetSize(s.width, s.height)
- }
- return nil
-}
-
-func (s *splitPaneLayout) SetRightPanel(panel Container) tea.Cmd {
- s.rightPanel = panel
- if s.width > 0 && s.height > 0 {
- return s.SetSize(s.width, s.height)
- }
- return nil
-}
-
-func (s *splitPaneLayout) SetBottomPanel(panel Container) tea.Cmd {
- s.bottomPanel = panel
- if s.width > 0 && s.height > 0 {
- return s.SetSize(s.width, s.height)
- }
- return nil
-}
-
-func (s *splitPaneLayout) ClearLeftPanel() tea.Cmd {
- s.leftPanel = nil
- if s.width > 0 && s.height > 0 {
- return s.SetSize(s.width, s.height)
- }
- return nil
-}
-
-func (s *splitPaneLayout) ClearRightPanel() tea.Cmd {
- s.rightPanel = nil
- if s.width > 0 && s.height > 0 {
- return s.SetSize(s.width, s.height)
- }
- return nil
-}
-
-func (s *splitPaneLayout) ClearBottomPanel() tea.Cmd {
- s.bottomPanel = nil
- if s.width > 0 && s.height > 0 {
- return s.SetSize(s.width, s.height)
- }
- return nil
-}
-
-func (s *splitPaneLayout) Bindings() []key.Binding {
- if s.leftPanel != nil {
- if b, ok := s.leftPanel.(Help); ok && s.leftPanel.IsFocused() {
- return b.Bindings()
- }
- }
- if s.rightPanel != nil {
- if b, ok := s.rightPanel.(Help); ok && s.rightPanel.IsFocused() {
- return b.Bindings()
- }
- }
- if s.bottomPanel != nil {
- if b, ok := s.bottomPanel.(Help); ok && s.bottomPanel.IsFocused() {
- return b.Bindings()
- }
- }
- return nil
-}
-
-func (s *splitPaneLayout) FocusPanel(panel LayoutPanel) tea.Cmd {
- panels := map[LayoutPanel]Container{
- LeftPanel: s.leftPanel,
- RightPanel: s.rightPanel,
- BottomPanel: s.bottomPanel,
- }
- var cmds []tea.Cmd
- for p, container := range panels {
- if container == nil {
- continue
- }
- if p == panel {
- cmds = append(cmds, container.Focus())
- } else {
- cmds = append(cmds, container.Blur())
- }
- }
- return tea.Batch(cmds...)
-}
-
-// SetOffset implements SplitPaneLayout.
-func (s *splitPaneLayout) SetOffset(x int, y int) {
- s.xOffset = x
- s.yOffset = y
-}
-
-func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
- layout := &splitPaneLayout{
- ratio: 0.8,
- verticalRatio: 0.92, // Default 90% for top section, 10% for bottom
- }
- for _, option := range options {
- option(layout)
- }
- return layout
-}
-
-func WithLeftPanel(panel Container) SplitPaneOption {
- return func(s *splitPaneLayout) {
- s.leftPanel = panel
- }
-}
-
-func WithRightPanel(panel Container) SplitPaneOption {
- return func(s *splitPaneLayout) {
- s.rightPanel = panel
- }
-}
-
-func WithRatio(ratio float64) SplitPaneOption {
- return func(s *splitPaneLayout) {
- s.ratio = ratio
- }
-}
-
-func WithBottomPanel(panel Container) SplitPaneOption {
- return func(s *splitPaneLayout) {
- s.bottomPanel = panel
- }
-}
-
-func WithVerticalRatio(ratio float64) SplitPaneOption {
- return func(s *splitPaneLayout) {
- s.verticalRatio = ratio
- }
-}
-
-func WithFixedBottomHeight(height int) SplitPaneOption {
- return func(s *splitPaneLayout) {
- s.fixedBottomHeight = height
- }
-}
-
-func WithFixedRightWidth(width int) SplitPaneOption {
- return func(s *splitPaneLayout) {
- s.fixedRightWidth = width
- }
-}
@@ -9,6 +9,8 @@ type KeyMap struct {
Up,
DownOneItem,
UpOneItem,
+ PageDown,
+ PageUp,
HalfPageDown,
HalfPageUp,
Home,
@@ -37,7 +39,14 @@ func DefaultKeyMap() KeyMap {
key.WithKeys("d"),
key.WithHelp("d", "half page down"),
),
- HalfPageUp: key.NewBinding(
+ PageDown: key.NewBinding(
+ key.WithKeys("pgdown", " ", "f"),
+ key.WithHelp("f/pgdn", "page down"),
+ ),
+ PageUp: key.NewBinding(
+ key.WithKeys("pgup", "b"),
+ key.WithHelp("b/pgup", "page up"),
+ ), HalfPageUp: key.NewBinding(
key.WithKeys("u"),
key.WithHelp("u", "half page up"),
),
@@ -332,6 +332,10 @@ func (m *model) handleKeyPress(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
m.scrollDown(m.listHeight() / 2)
case key.Matches(msg, m.keyMap.HalfPageUp):
m.scrollUp(m.listHeight() / 2)
+ case key.Matches(msg, m.keyMap.PageDown):
+ m.scrollDown(m.listHeight())
+ case key.Matches(msg, m.keyMap.PageUp):
+ m.scrollUp(m.listHeight())
case key.Matches(msg, m.keyMap.Home):
return m, m.goToTop()
case key.Matches(msg, m.keyMap.End):
@@ -98,13 +98,12 @@ func (m *statusCmp) SetKeyMap(keyMap help.KeyMap) {
m.keyMap = keyMap
}
-func NewStatusCmp(keyMap help.KeyMap) StatusCmp {
+func NewStatusCmp() StatusCmp {
t := styles.CurrentTheme()
help := help.New()
help.Styles = t.S().Help
return &statusCmp{
messageTTL: 5 * time.Second,
help: help,
- keyMap: keyMap,
}
}
@@ -20,8 +20,8 @@ func DefaultKeyMap() KeyMap {
key.WithHelp("ctrl+c", "quit"),
),
Help: key.NewBinding(
- key.WithKeys("ctrl+?", "ctrl+_", "ctrl+/"),
- key.WithHelp("ctrl+?", "more"),
+ key.WithKeys("ctrl+g"),
+ key.WithHelp("ctrl+g", "more"),
),
Commands: key.NewBinding(
key.WithKeys("ctrl+p"),
@@ -33,62 +33,3 @@ func DefaultKeyMap() KeyMap {
),
}
}
-
-// FullHelp implements help.KeyMap.
-func (k KeyMap) FullHelp() [][]key.Binding {
- m := [][]key.Binding{}
- slice := []key.Binding{
- k.Commands,
- k.Sessions,
- k.Quit,
- k.Help,
- }
- slice = k.prependEscAndTab(slice)
- slice = append(slice, k.pageBindings...)
- // remove duplicates
- seen := make(map[string]bool)
- cleaned := []key.Binding{}
- for _, b := range slice {
- if !seen[b.Help().Key] {
- seen[b.Help().Key] = true
- cleaned = append(cleaned, b)
- }
- }
-
- for i := 0; i < len(cleaned); i += 3 {
- end := min(i+3, len(cleaned))
- m = append(m, cleaned[i:end])
- }
- return m
-}
-
-func (k KeyMap) prependEscAndTab(bindings []key.Binding) []key.Binding {
- var cancel key.Binding
- var tab key.Binding
- for _, b := range k.pageBindings {
- if b.Help().Key == "esc" {
- cancel = b
- }
- if b.Help().Key == "tab" {
- tab = b
- }
- }
- if tab.Help().Key != "" {
- bindings = append([]key.Binding{tab}, bindings...)
- }
- if cancel.Help().Key != "" {
- bindings = append([]key.Binding{cancel}, bindings...)
- }
- return bindings
-}
-
-// ShortHelp implements help.KeyMap.
-func (k KeyMap) ShortHelp() []key.Binding {
- bindings := []key.Binding{
- k.Commands,
- k.Sessions,
- k.Quit,
- k.Help,
- }
- return k.prependEscAndTab(bindings)
-}
@@ -2,8 +2,10 @@ package chat
import (
"context"
+ "runtime"
"time"
+ "github.com/charmbracelet/bubbles/v2/help"
"github.com/charmbracelet/bubbles/v2/key"
"github.com/charmbracelet/bubbles/v2/spinner"
tea "github.com/charmbracelet/bubbletea/v2"
@@ -20,6 +22,7 @@ import (
"github.com/charmbracelet/crush/internal/tui/components/chat/sidebar"
"github.com/charmbracelet/crush/internal/tui/components/chat/splash"
"github.com/charmbracelet/crush/internal/tui/components/completions"
+ "github.com/charmbracelet/crush/internal/tui/components/core"
"github.com/charmbracelet/crush/internal/tui/components/core/layout"
"github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
"github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
@@ -82,6 +85,7 @@ type chatPage struct {
width, height int
detailsWidth, detailsHeight int
app *app.App
+ keyboardEnhancements tea.KeyboardEnhancementsMsg
// Layout state
compact bool
@@ -103,6 +107,8 @@ type chatPage struct {
showingDetails bool
isCanceling bool
splashFullScreen bool
+ isOnboarding bool
+ isProjectInit bool
}
func New(app *app.App) ChatPage {
@@ -129,10 +135,12 @@ func (p *chatPage) Init() tea.Cmd {
if !config.HasInitialDataConfig() {
// First-time setup: show model selection
p.splash.SetOnboarding(true)
+ p.isOnboarding = true
p.splashFullScreen = true
} else if b, _ := config.ProjectNeedsInitialization(); b {
// Project needs CRUSH.md initialization
p.splash.SetProjectInit(true)
+ p.isProjectInit = true
p.splashFullScreen = true
} else {
// Ready to chat: focus editor, splash in background
@@ -153,9 +161,8 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyboardEnhancementsMsg:
- m, cmd := p.editor.Update(msg)
- p.editor = m.(editor.Editor)
- return p, cmd
+ p.keyboardEnhancements = msg
+ return p, nil
case tea.WindowSizeMsg:
return p, p.SetSize(msg.Width, msg.Height)
case CancelTimerExpiredMsg:
@@ -237,6 +244,8 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if err != nil {
return p, util.ReportError(err)
}
+ p.isOnboarding = false
+ p.isProjectInit = false
p.focusedPane = PanelTypeEditor
return p, p.SetSize(p.width, p.height)
case tea.KeyPressMsg:
@@ -579,6 +588,231 @@ func (p *chatPage) Bindings() []key.Binding {
return bindings
}
+func (a *chatPage) Help() help.KeyMap {
+ var shortList []key.Binding
+ var fullList [][]key.Binding
+ switch {
+ case a.isOnboarding && !a.splash.IsShowingAPIKey():
+ shortList = append(shortList,
+ // Choose model
+ key.NewBinding(
+ key.WithKeys("up", "down"),
+ key.WithHelp("↑/↓", "choose"),
+ ),
+ // Accept selection
+ key.NewBinding(
+ key.WithKeys("enter", "ctrl+y"),
+ key.WithHelp("enter", "accept"),
+ ),
+ // Quit
+ key.NewBinding(
+ key.WithKeys("ctrl+c"),
+ key.WithHelp("ctrl+c", "quit"),
+ ),
+ )
+ // keep them the same
+ for _, v := range shortList {
+ fullList = append(fullList, []key.Binding{v})
+ }
+ case a.isOnboarding && a.splash.IsShowingAPIKey():
+ var pasteKey key.Binding
+ if runtime.GOOS != "darwin" {
+ pasteKey = key.NewBinding(
+ key.WithKeys("ctrl+v"),
+ key.WithHelp("ctrl+v", "paste API key"),
+ )
+ } else {
+ pasteKey = key.NewBinding(
+ key.WithKeys("cmd+v"),
+ key.WithHelp("cmd+v", "paste API key"),
+ )
+ }
+ shortList = append(shortList,
+ // Go back
+ key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "back"),
+ ),
+ // Paste
+ pasteKey,
+ // Quit
+ key.NewBinding(
+ key.WithKeys("ctrl+c"),
+ key.WithHelp("ctrl+c", "quit"),
+ ),
+ )
+ // keep them the same
+ for _, v := range shortList {
+ fullList = append(fullList, []key.Binding{v})
+ }
+ case a.isProjectInit:
+ shortList = append(shortList,
+ key.NewBinding(
+ key.WithKeys("ctrl+c"),
+ key.WithHelp("ctrl+c", "quit"),
+ ),
+ )
+ // keep them the same
+ for _, v := range shortList {
+ fullList = append(fullList, []key.Binding{v})
+ }
+ default:
+ if a.app.CoderAgent != nil && a.app.CoderAgent.IsBusy() {
+ cancelBinding := key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "cancel"),
+ )
+ if a.isCanceling {
+ cancelBinding = key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "press again to cancel"),
+ )
+ }
+ shortList = append(shortList, cancelBinding)
+ fullList = append(fullList,
+ []key.Binding{
+ cancelBinding,
+ },
+ )
+ }
+ globalBindings := []key.Binding{}
+ // we are in a session
+ if a.session.ID != "" {
+ tabKey := key.NewBinding(
+ key.WithKeys("tab"),
+ key.WithHelp("tab", "focus chat"),
+ )
+ if a.focusedPane == PanelTypeChat {
+ tabKey = key.NewBinding(
+ key.WithKeys("tab"),
+ key.WithHelp("tab", "focus editor"),
+ )
+ }
+ shortList = append(shortList, tabKey)
+ globalBindings = append(globalBindings, tabKey)
+ }
+ commandsBinding := key.NewBinding(
+ key.WithKeys("ctrl+p"),
+ key.WithHelp("ctrl+p", "commands"),
+ )
+ helpBinding := key.NewBinding(
+ key.WithKeys("ctrl+g"),
+ key.WithHelp("ctrl+g", "more"),
+ )
+ globalBindings = append(globalBindings, commandsBinding)
+ globalBindings = append(globalBindings,
+ key.NewBinding(
+ key.WithKeys("ctrl+s"),
+ key.WithHelp("ctrl+s", "sessions"),
+ ),
+ )
+ if a.session.ID != "" {
+ globalBindings = append(globalBindings,
+ key.NewBinding(
+ key.WithKeys("ctrl+n"),
+ key.WithHelp("ctrl+n", "new sessions"),
+ ))
+ }
+ shortList = append(shortList,
+ // Commands
+ commandsBinding,
+ )
+ fullList = append(fullList, globalBindings)
+
+ if a.focusedPane == PanelTypeChat {
+ shortList = append(shortList,
+ key.NewBinding(
+ key.WithKeys("up", "down"),
+ key.WithHelp("↑↓", "scroll"),
+ ),
+ )
+ fullList = append(fullList,
+ []key.Binding{
+ key.NewBinding(
+ key.WithKeys("up", "down"),
+ key.WithHelp("↑↓", "scroll"),
+ ),
+ key.NewBinding(
+ key.WithKeys("shift+up", "shift+down"),
+ key.WithHelp("shift+↑↓", "next/prev item"),
+ ),
+ key.NewBinding(
+ key.WithKeys("pgup", "b"),
+ key.WithHelp("b/pgup", "page up"),
+ ),
+ key.NewBinding(
+ key.WithKeys("pgdown", " ", "f"),
+ key.WithHelp("f/pgdn", "page down"),
+ ),
+ },
+ []key.Binding{
+ key.NewBinding(
+ key.WithKeys("u"),
+ key.WithHelp("u", "half page up"),
+ ),
+ key.NewBinding(
+ key.WithKeys("d"),
+ key.WithHelp("d", "half page down"),
+ ),
+ key.NewBinding(
+ key.WithKeys("g", "home"),
+ key.WithHelp("g", "hone"),
+ ),
+ key.NewBinding(
+ key.WithKeys("G", "end"),
+ key.WithHelp("G", "end"),
+ ),
+ },
+ )
+ } else if a.focusedPane == PanelTypeEditor {
+ newLineBinding := 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"),
+ )
+ if a.keyboardEnhancements.SupportsKeyDisambiguation() {
+ newLineBinding.SetHelp("shift+enter", newLineBinding.Help().Desc)
+ }
+ shortList = append(shortList, newLineBinding)
+ fullList = append(fullList,
+ []key.Binding{
+ newLineBinding,
+ key.NewBinding(
+ key.WithKeys("ctrl+f"),
+ key.WithHelp("ctrl+f", "add image"),
+ ),
+ key.NewBinding(
+ key.WithKeys("/"),
+ key.WithHelp("/", "add file"),
+ ),
+ key.NewBinding(
+ key.WithKeys("ctrl+e"),
+ key.WithHelp("ctrl+e", "open editor"),
+ ),
+ })
+ }
+ shortList = append(shortList,
+ // Quit
+ key.NewBinding(
+ key.WithKeys("ctrl+c"),
+ key.WithHelp("ctrl+c", "quit"),
+ ),
+ // Help
+ helpBinding,
+ )
+ fullList = append(fullList, []key.Binding{
+ key.NewBinding(
+ key.WithKeys("ctrl+g"),
+ key.WithHelp("ctrl+g", "less"),
+ ),
+ })
+ }
+
+ return core.NewSimpleHelp(shortList, fullList)
+}
+
func (p *chatPage) IsChatFocused() bool {
return p.focusedPane == PanelTypeChat
}
@@ -13,6 +13,7 @@ import (
"github.com/charmbracelet/crush/internal/pubsub"
cmpChat "github.com/charmbracelet/crush/internal/tui/components/chat"
"github.com/charmbracelet/crush/internal/tui/components/completions"
+ "github.com/charmbracelet/crush/internal/tui/components/core"
"github.com/charmbracelet/crush/internal/tui/components/core/layout"
"github.com/charmbracelet/crush/internal/tui/components/core/status"
"github.com/charmbracelet/crush/internal/tui/components/dialogs"
@@ -257,7 +258,7 @@ func (a *appModel) handleWindowResize(width, height int) tea.Cmd {
var cmds []tea.Cmd
a.wWidth, a.wHeight = width, height
if a.showingFullHelp {
- height -= 4
+ height -= 5
} else {
height -= 2
}
@@ -384,10 +385,9 @@ func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
// View renders the complete application interface including pages, dialogs, and overlays.
func (a *appModel) View() tea.View {
page := a.pages[a.currentPage]
- if withHelp, ok := page.(layout.Help); ok {
- a.keyMap.pageBindings = withHelp.Bindings()
+ if withHelp, ok := page.(core.KeyMapHelp); ok {
+ a.status.SetKeyMap(withHelp.Help())
}
- a.status.SetKeyMap(a.keyMap)
pageView := page.View()
components := []string{
pageView,
@@ -447,7 +447,7 @@ func New(app *app.App) tea.Model {
model := &appModel{
currentPage: chat.ChatPageID,
app: app,
- status: status.NewStatusCmp(keyMap),
+ status: status.NewStatusCmp(),
loadedPages: make(map[page.PageID]bool),
keyMap: keyMap,