1package chat
2
3import (
4 "context"
5 "time"
6
7 "github.com/charmbracelet/bubbles/v2/help"
8 "github.com/charmbracelet/bubbles/v2/key"
9 "github.com/charmbracelet/bubbles/v2/spinner"
10 tea "github.com/charmbracelet/bubbletea/v2"
11 "github.com/charmbracelet/crush/internal/app"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/history"
14 "github.com/charmbracelet/crush/internal/message"
15 "github.com/charmbracelet/crush/internal/pubsub"
16 "github.com/charmbracelet/crush/internal/session"
17 "github.com/charmbracelet/crush/internal/tui/components/anim"
18 "github.com/charmbracelet/crush/internal/tui/components/chat"
19 "github.com/charmbracelet/crush/internal/tui/components/chat/editor"
20 "github.com/charmbracelet/crush/internal/tui/components/chat/header"
21 "github.com/charmbracelet/crush/internal/tui/components/chat/sidebar"
22 "github.com/charmbracelet/crush/internal/tui/components/chat/splash"
23 "github.com/charmbracelet/crush/internal/tui/components/completions"
24 "github.com/charmbracelet/crush/internal/tui/components/core"
25 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
26 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
27 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
28 "github.com/charmbracelet/crush/internal/tui/page"
29 "github.com/charmbracelet/crush/internal/tui/styles"
30 "github.com/charmbracelet/crush/internal/tui/util"
31 "github.com/charmbracelet/crush/internal/version"
32 "github.com/charmbracelet/lipgloss/v2"
33)
34
35var ChatPageID page.PageID = "chat"
36
37type (
38 OpenFilePickerMsg struct{}
39 ChatFocusedMsg struct {
40 Focused bool
41 }
42 CancelTimerExpiredMsg struct{}
43)
44
45type PanelType string
46
47const (
48 PanelTypeChat PanelType = "chat"
49 PanelTypeEditor PanelType = "editor"
50 PanelTypeSplash PanelType = "splash"
51)
52
53const (
54 CompactModeBreakpoint = 120 // Width at which the chat page switches to compact mode
55 EditorHeight = 5 // Height of the editor input area including padding
56 SideBarWidth = 31 // Width of the sidebar
57 SideBarDetailsPadding = 1 // Padding for the sidebar details section
58 HeaderHeight = 1 // Height of the header
59
60 // Layout constants for borders and padding
61 BorderWidth = 1 // Width of component borders
62 LeftRightBorders = 2 // Left + right border width (1 + 1)
63 TopBottomBorders = 2 // Top + bottom border width (1 + 1)
64 DetailsPositioning = 2 // Positioning adjustment for details panel
65
66 // Timing constants
67 CancelTimerDuration = 2 * time.Second // Duration before cancel timer expires
68)
69
70type ChatPage interface {
71 util.Model
72 layout.Help
73 IsChatFocused() bool
74}
75
76// cancelTimerCmd creates a command that expires the cancel timer
77func cancelTimerCmd() tea.Cmd {
78 return tea.Tick(CancelTimerDuration, func(time.Time) tea.Msg {
79 return CancelTimerExpiredMsg{}
80 })
81}
82
83type chatPage struct {
84 width, height int
85 detailsWidth, detailsHeight int
86 app *app.App
87 keyboardEnhancements tea.KeyboardEnhancementsMsg
88
89 // Layout state
90 compact bool
91 forceCompact bool
92 focusedPane PanelType
93
94 // Session
95 session session.Session
96 keyMap KeyMap
97
98 // Components
99 header header.Header
100 sidebar sidebar.Sidebar
101 chat chat.MessageListCmp
102 editor editor.Editor
103 splash splash.Splash
104
105 // Simple state flags
106 showingDetails bool
107 isCanceling bool
108 splashFullScreen bool
109 isOnboarding bool
110 isProjectInit bool
111}
112
113func New(app *app.App) ChatPage {
114 return &chatPage{
115 app: app,
116 keyMap: DefaultKeyMap(),
117 header: header.New(app.LSPClients),
118 sidebar: sidebar.New(app.History, app.LSPClients, false),
119 chat: chat.New(app),
120 editor: editor.New(app),
121 splash: splash.New(),
122 focusedPane: PanelTypeSplash,
123 }
124}
125
126func (p *chatPage) Init() tea.Cmd {
127 cfg := config.Get()
128 compact := cfg.Options.TUI.CompactMode
129 p.compact = compact
130 p.forceCompact = compact
131 p.sidebar.SetCompactMode(p.compact)
132
133 // Set splash state based on config
134 if !config.HasInitialDataConfig() {
135 // First-time setup: show model selection
136 p.splash.SetOnboarding(true)
137 p.isOnboarding = true
138 p.splashFullScreen = true
139 } else if b, _ := config.ProjectNeedsInitialization(); b {
140 // Project needs CRUSH.md initialization
141 p.splash.SetProjectInit(true)
142 p.isProjectInit = true
143 p.splashFullScreen = true
144 } else {
145 // Ready to chat: focus editor, splash in background
146 p.focusedPane = PanelTypeEditor
147 p.splashFullScreen = false
148 }
149
150 return tea.Batch(
151 p.header.Init(),
152 p.sidebar.Init(),
153 p.chat.Init(),
154 p.editor.Init(),
155 p.splash.Init(),
156 )
157}
158
159func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
160 var cmds []tea.Cmd
161 switch msg := msg.(type) {
162 case tea.KeyboardEnhancementsMsg:
163 p.keyboardEnhancements = msg
164 return p, nil
165 case tea.WindowSizeMsg:
166 return p, p.SetSize(msg.Width, msg.Height)
167 case CancelTimerExpiredMsg:
168 p.isCanceling = false
169 return p, nil
170 case chat.SendMsg:
171 return p, p.sendMessage(msg.Text, msg.Attachments)
172 case chat.SessionSelectedMsg:
173 return p, p.setSession(msg)
174 case commands.ToggleCompactModeMsg:
175 p.forceCompact = !p.forceCompact
176 var cmd tea.Cmd
177 if p.forceCompact {
178 p.setCompactMode(true)
179 cmd = p.updateCompactConfig(true)
180 } else if p.width >= CompactModeBreakpoint {
181 p.setCompactMode(false)
182 cmd = p.updateCompactConfig(false)
183 }
184 return p, tea.Batch(p.SetSize(p.width, p.height), cmd)
185 case pubsub.Event[session.Session]:
186 u, cmd := p.header.Update(msg)
187 p.header = u.(header.Header)
188 cmds = append(cmds, cmd)
189 u, cmd = p.sidebar.Update(msg)
190 p.sidebar = u.(sidebar.Sidebar)
191 cmds = append(cmds, cmd)
192 return p, tea.Batch(cmds...)
193 case chat.SessionClearedMsg:
194 u, cmd := p.header.Update(msg)
195 p.header = u.(header.Header)
196 cmds = append(cmds, cmd)
197 u, cmd = p.sidebar.Update(msg)
198 p.sidebar = u.(sidebar.Sidebar)
199 cmds = append(cmds, cmd)
200 u, cmd = p.chat.Update(msg)
201 p.chat = u.(chat.MessageListCmp)
202 cmds = append(cmds, cmd)
203 return p, tea.Batch(cmds...)
204 case filepicker.FilePickedMsg,
205 completions.CompletionsClosedMsg,
206 completions.SelectCompletionMsg:
207 u, cmd := p.editor.Update(msg)
208 p.editor = u.(editor.Editor)
209 cmds = append(cmds, cmd)
210 return p, tea.Batch(cmds...)
211
212 case pubsub.Event[message.Message],
213 anim.StepMsg,
214 spinner.TickMsg:
215 u, cmd := p.chat.Update(msg)
216 p.chat = u.(chat.MessageListCmp)
217 cmds = append(cmds, cmd)
218 return p, tea.Batch(cmds...)
219
220 case pubsub.Event[history.File], sidebar.SessionFilesMsg:
221 u, cmd := p.sidebar.Update(msg)
222 p.sidebar = u.(sidebar.Sidebar)
223 cmds = append(cmds, cmd)
224 return p, tea.Batch(cmds...)
225
226 case commands.CommandRunCustomMsg:
227 if p.app.CoderAgent.IsBusy() {
228 return p, util.ReportWarn("Agent is busy, please wait before executing a command...")
229 }
230
231 cmd := p.sendMessage(msg.Content, nil)
232 if cmd != nil {
233 return p, cmd
234 }
235 case splash.OnboardingCompleteMsg:
236 p.splashFullScreen = false
237 if b, _ := config.ProjectNeedsInitialization(); b {
238 p.splash.SetProjectInit(true)
239 p.splashFullScreen = true
240 return p, p.SetSize(p.width, p.height)
241 }
242 err := p.app.InitCoderAgent()
243 if err != nil {
244 return p, util.ReportError(err)
245 }
246 p.isOnboarding = false
247 p.isProjectInit = false
248 p.focusedPane = PanelTypeEditor
249 return p, p.SetSize(p.width, p.height)
250 case tea.KeyPressMsg:
251 switch {
252 case key.Matches(msg, p.keyMap.NewSession):
253 return p, p.newSession()
254 case key.Matches(msg, p.keyMap.AddAttachment):
255 agentCfg := config.Get().Agents["coder"]
256 model := config.Get().GetModelByType(agentCfg.Model)
257 if model.SupportsImages {
258 return p, util.CmdHandler(OpenFilePickerMsg{})
259 } else {
260 return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Model)
261 }
262 case key.Matches(msg, p.keyMap.Tab):
263 if p.session.ID == "" {
264 u, cmd := p.splash.Update(msg)
265 p.splash = u.(splash.Splash)
266 return p, cmd
267 }
268 p.changeFocus()
269 return p, nil
270 case key.Matches(msg, p.keyMap.Cancel):
271 if p.session.ID != "" && p.app.CoderAgent.IsBusy() {
272 return p, p.cancel()
273 }
274 case key.Matches(msg, p.keyMap.Details):
275 p.showDetails()
276 return p, nil
277 }
278
279 switch p.focusedPane {
280 case PanelTypeChat:
281 u, cmd := p.chat.Update(msg)
282 p.chat = u.(chat.MessageListCmp)
283 cmds = append(cmds, cmd)
284 case PanelTypeEditor:
285 u, cmd := p.editor.Update(msg)
286 p.editor = u.(editor.Editor)
287 cmds = append(cmds, cmd)
288 case PanelTypeSplash:
289 u, cmd := p.splash.Update(msg)
290 p.splash = u.(splash.Splash)
291 cmds = append(cmds, cmd)
292 }
293 case tea.PasteMsg:
294 switch p.focusedPane {
295 case PanelTypeEditor:
296 u, cmd := p.editor.Update(msg)
297 p.editor = u.(editor.Editor)
298 cmds = append(cmds, cmd)
299 return p, tea.Batch(cmds...)
300 case PanelTypeChat:
301 u, cmd := p.chat.Update(msg)
302 p.chat = u.(chat.MessageListCmp)
303 cmds = append(cmds, cmd)
304 return p, tea.Batch(cmds...)
305 case PanelTypeSplash:
306 u, cmd := p.splash.Update(msg)
307 p.splash = u.(splash.Splash)
308 cmds = append(cmds, cmd)
309 return p, tea.Batch(cmds...)
310 }
311 }
312 return p, tea.Batch(cmds...)
313}
314
315func (p *chatPage) Cursor() *tea.Cursor {
316 switch p.focusedPane {
317 case PanelTypeEditor:
318 return p.editor.Cursor()
319 case PanelTypeSplash:
320 return p.splash.Cursor()
321 default:
322 return nil
323 }
324}
325
326func (p *chatPage) View() string {
327 var chatView string
328 t := styles.CurrentTheme()
329
330 if p.session.ID == "" {
331 splashView := p.splash.View()
332 // Full screen during onboarding or project initialization
333 if p.splashFullScreen {
334 chatView = splashView
335 } else {
336 // Show splash + editor for new message state
337 editorView := p.editor.View()
338 chatView = lipgloss.JoinVertical(
339 lipgloss.Left,
340 t.S().Base.Render(splashView),
341 editorView,
342 )
343 }
344 } else {
345 messagesView := p.chat.View()
346 editorView := p.editor.View()
347 if p.compact {
348 headerView := p.header.View()
349 chatView = lipgloss.JoinVertical(
350 lipgloss.Left,
351 headerView,
352 messagesView,
353 editorView,
354 )
355 } else {
356 sidebarView := p.sidebar.View()
357 messages := lipgloss.JoinHorizontal(
358 lipgloss.Left,
359 messagesView,
360 sidebarView,
361 )
362 chatView = lipgloss.JoinVertical(
363 lipgloss.Left,
364 messages,
365 p.editor.View(),
366 )
367 }
368 }
369
370 layers := []*lipgloss.Layer{
371 lipgloss.NewLayer(chatView).X(0).Y(0),
372 }
373
374 if p.showingDetails {
375 style := t.S().Base.
376 Width(p.detailsWidth).
377 Border(lipgloss.RoundedBorder()).
378 BorderForeground(t.BorderFocus)
379 version := t.S().Subtle.Width(p.detailsWidth - 2).AlignHorizontal(lipgloss.Right).Render(version.Version)
380 details := style.Render(
381 lipgloss.JoinVertical(
382 lipgloss.Left,
383 p.sidebar.View(),
384 version,
385 ),
386 )
387 layers = append(layers, lipgloss.NewLayer(details).X(1).Y(1))
388 }
389 canvas := lipgloss.NewCanvas(
390 layers...,
391 )
392 return canvas.Render()
393}
394
395func (p *chatPage) updateCompactConfig(compact bool) tea.Cmd {
396 return func() tea.Msg {
397 err := config.Get().SetCompactMode(compact)
398 if err != nil {
399 return util.InfoMsg{
400 Type: util.InfoTypeError,
401 Msg: "Failed to update compact mode configuration: " + err.Error(),
402 }
403 }
404 return nil
405 }
406}
407
408func (p *chatPage) setCompactMode(compact bool) {
409 if p.compact == compact {
410 return
411 }
412 p.compact = compact
413 if compact {
414 p.compact = true
415 p.sidebar.SetCompactMode(true)
416 } else {
417 p.compact = false
418 p.showingDetails = false
419 p.sidebar.SetCompactMode(false)
420 }
421}
422
423func (p *chatPage) handleCompactMode(newWidth int) {
424 if p.forceCompact {
425 return
426 }
427 if newWidth < CompactModeBreakpoint && !p.compact {
428 p.setCompactMode(true)
429 }
430 if newWidth >= CompactModeBreakpoint && p.compact {
431 p.setCompactMode(false)
432 }
433}
434
435func (p *chatPage) SetSize(width, height int) tea.Cmd {
436 p.handleCompactMode(width)
437 p.width = width
438 p.height = height
439 var cmds []tea.Cmd
440
441 if p.session.ID == "" {
442 if p.splashFullScreen {
443 cmds = append(cmds, p.splash.SetSize(width, height))
444 } else {
445 cmds = append(cmds, p.splash.SetSize(width, height-EditorHeight))
446 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
447 cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
448 }
449 } else {
450 if p.compact {
451 cmds = append(cmds, p.chat.SetSize(width, height-EditorHeight-HeaderHeight))
452 p.detailsWidth = width - DetailsPositioning
453 cmds = append(cmds, p.sidebar.SetSize(p.detailsWidth-LeftRightBorders, p.detailsHeight-TopBottomBorders))
454 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
455 cmds = append(cmds, p.header.SetWidth(width-BorderWidth))
456 } else {
457 cmds = append(cmds, p.chat.SetSize(width-SideBarWidth, height-EditorHeight))
458 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
459 cmds = append(cmds, p.sidebar.SetSize(SideBarWidth, height-EditorHeight))
460 }
461 cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
462 }
463 return tea.Batch(cmds...)
464}
465
466func (p *chatPage) newSession() tea.Cmd {
467 if p.session.ID == "" {
468 return nil
469 }
470
471 p.session = session.Session{}
472 p.focusedPane = PanelTypeEditor
473 p.isCanceling = false
474 return tea.Batch(
475 util.CmdHandler(chat.SessionClearedMsg{}),
476 p.SetSize(p.width, p.height),
477 )
478}
479
480func (p *chatPage) setSession(session session.Session) tea.Cmd {
481 if p.session.ID == session.ID {
482 return nil
483 }
484
485 var cmds []tea.Cmd
486 p.session = session
487
488 cmds = append(cmds, p.SetSize(p.width, p.height))
489 cmds = append(cmds, p.chat.SetSession(session))
490 cmds = append(cmds, p.sidebar.SetSession(session))
491 cmds = append(cmds, p.header.SetSession(session))
492 cmds = append(cmds, p.editor.SetSession(session))
493
494 return tea.Sequence(cmds...)
495}
496
497func (p *chatPage) changeFocus() {
498 if p.session.ID == "" {
499 return
500 }
501 switch p.focusedPane {
502 case PanelTypeChat:
503 p.focusedPane = PanelTypeEditor
504 p.editor.Focus()
505 p.chat.Blur()
506 case PanelTypeEditor:
507 p.focusedPane = PanelTypeChat
508 p.chat.Focus()
509 p.editor.Blur()
510 }
511}
512
513func (p *chatPage) cancel() tea.Cmd {
514 if p.isCanceling {
515 p.isCanceling = false
516 p.app.CoderAgent.Cancel(p.session.ID)
517 return nil
518 }
519
520 p.isCanceling = true
521 return cancelTimerCmd()
522}
523
524func (p *chatPage) showDetails() {
525 if p.session.ID == "" || !p.compact {
526 return
527 }
528 p.showingDetails = !p.showingDetails
529 p.header.SetDetailsOpen(p.showingDetails)
530}
531
532func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
533 session := p.session
534 var cmds []tea.Cmd
535 if p.session.ID == "" {
536 newSession, err := p.app.Sessions.Create(context.Background(), "New Session")
537 if err != nil {
538 return util.ReportError(err)
539 }
540 session = newSession
541 cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
542 }
543 _, err := p.app.CoderAgent.Run(context.Background(), session.ID, text, attachments...)
544 if err != nil {
545 return util.ReportError(err)
546 }
547 return tea.Batch(cmds...)
548}
549
550func (p *chatPage) Bindings() []key.Binding {
551 bindings := []key.Binding{
552 p.keyMap.NewSession,
553 p.keyMap.AddAttachment,
554 }
555 if p.app.CoderAgent != nil && p.app.CoderAgent.IsBusy() {
556 cancelBinding := p.keyMap.Cancel
557 if p.isCanceling {
558 cancelBinding = key.NewBinding(
559 key.WithKeys("esc"),
560 key.WithHelp("esc", "press again to cancel"),
561 )
562 }
563 bindings = append([]key.Binding{cancelBinding}, bindings...)
564 }
565
566 switch p.focusedPane {
567 case PanelTypeChat:
568 bindings = append([]key.Binding{
569 key.NewBinding(
570 key.WithKeys("tab"),
571 key.WithHelp("tab", "focus editor"),
572 ),
573 }, bindings...)
574 bindings = append(bindings, p.chat.Bindings()...)
575 case PanelTypeEditor:
576 bindings = append([]key.Binding{
577 key.NewBinding(
578 key.WithKeys("tab"),
579 key.WithHelp("tab", "focus chat"),
580 ),
581 }, bindings...)
582 bindings = append(bindings, p.editor.Bindings()...)
583 case PanelTypeSplash:
584 bindings = append(bindings, p.splash.Bindings()...)
585 }
586
587 return bindings
588}
589
590func (a *chatPage) Help() help.KeyMap {
591 var shortList []key.Binding
592 var fullList [][]key.Binding
593 switch {
594 case a.isOnboarding && !a.splash.IsShowingAPIKey():
595 shortList = append(shortList,
596 // Choose model
597 key.NewBinding(
598 key.WithKeys("up", "down"),
599 key.WithHelp("↑/↓", "choose"),
600 ),
601 // Accept selection
602 key.NewBinding(
603 key.WithKeys("enter", "ctrl+y"),
604 key.WithHelp("enter", "accept"),
605 ),
606 // Quit
607 key.NewBinding(
608 key.WithKeys("ctrl+c"),
609 key.WithHelp("ctrl+c", "quit"),
610 ),
611 )
612 // keep them the same
613 for _, v := range shortList {
614 fullList = append(fullList, []key.Binding{v})
615 }
616 case a.isOnboarding && a.splash.IsShowingAPIKey():
617 shortList = append(shortList,
618 // Go back
619 key.NewBinding(
620 key.WithKeys("esc"),
621 key.WithHelp("esc", "back"),
622 ),
623 // Quit
624 key.NewBinding(
625 key.WithKeys("ctrl+c"),
626 key.WithHelp("ctrl+c", "quit"),
627 ),
628 )
629 // keep them the same
630 for _, v := range shortList {
631 fullList = append(fullList, []key.Binding{v})
632 }
633 case a.isProjectInit:
634 shortList = append(shortList,
635 key.NewBinding(
636 key.WithKeys("ctrl+c"),
637 key.WithHelp("ctrl+c", "quit"),
638 ),
639 )
640 // keep them the same
641 for _, v := range shortList {
642 fullList = append(fullList, []key.Binding{v})
643 }
644 default:
645 if a.editor.IsCompletionsOpen() {
646 shortList = append(shortList,
647 key.NewBinding(
648 key.WithKeys("tab", "enter"),
649 key.WithHelp("tab/enter", "complete"),
650 ),
651 key.NewBinding(
652 key.WithKeys("esc"),
653 key.WithHelp("esc", "cancel"),
654 ),
655 key.NewBinding(
656 key.WithKeys("up", "down"),
657 key.WithHelp("↑/↓", "choose"),
658 ),
659 )
660 for _, v := range shortList {
661 fullList = append(fullList, []key.Binding{v})
662 }
663 return core.NewSimpleHelp(shortList, fullList)
664 }
665 if a.app.CoderAgent != nil && a.app.CoderAgent.IsBusy() {
666 cancelBinding := key.NewBinding(
667 key.WithKeys("esc"),
668 key.WithHelp("esc", "cancel"),
669 )
670 if a.isCanceling {
671 cancelBinding = key.NewBinding(
672 key.WithKeys("esc"),
673 key.WithHelp("esc", "press again to cancel"),
674 )
675 }
676 shortList = append(shortList, cancelBinding)
677 fullList = append(fullList,
678 []key.Binding{
679 cancelBinding,
680 },
681 )
682 }
683 globalBindings := []key.Binding{}
684 // we are in a session
685 if a.session.ID != "" {
686 tabKey := key.NewBinding(
687 key.WithKeys("tab"),
688 key.WithHelp("tab", "focus chat"),
689 )
690 if a.focusedPane == PanelTypeChat {
691 tabKey = key.NewBinding(
692 key.WithKeys("tab"),
693 key.WithHelp("tab", "focus editor"),
694 )
695 }
696 shortList = append(shortList, tabKey)
697 globalBindings = append(globalBindings, tabKey)
698 }
699 commandsBinding := key.NewBinding(
700 key.WithKeys("ctrl+p"),
701 key.WithHelp("ctrl+p", "commands"),
702 )
703 helpBinding := key.NewBinding(
704 key.WithKeys("ctrl+g"),
705 key.WithHelp("ctrl+g", "more"),
706 )
707 globalBindings = append(globalBindings, commandsBinding)
708 globalBindings = append(globalBindings,
709 key.NewBinding(
710 key.WithKeys("ctrl+s"),
711 key.WithHelp("ctrl+s", "sessions"),
712 ),
713 )
714 if a.session.ID != "" {
715 globalBindings = append(globalBindings,
716 key.NewBinding(
717 key.WithKeys("ctrl+n"),
718 key.WithHelp("ctrl+n", "new sessions"),
719 ))
720 }
721 shortList = append(shortList,
722 // Commands
723 commandsBinding,
724 )
725 fullList = append(fullList, globalBindings)
726
727 if a.focusedPane == PanelTypeChat {
728 shortList = append(shortList,
729 key.NewBinding(
730 key.WithKeys("up", "down"),
731 key.WithHelp("↑↓", "scroll"),
732 ),
733 )
734 fullList = append(fullList,
735 []key.Binding{
736 key.NewBinding(
737 key.WithKeys("up", "down"),
738 key.WithHelp("↑↓", "scroll"),
739 ),
740 key.NewBinding(
741 key.WithKeys("shift+up", "shift+down"),
742 key.WithHelp("shift+↑↓", "next/prev item"),
743 ),
744 key.NewBinding(
745 key.WithKeys("pgup", "b"),
746 key.WithHelp("b/pgup", "page up"),
747 ),
748 key.NewBinding(
749 key.WithKeys("pgdown", " ", "f"),
750 key.WithHelp("f/pgdn", "page down"),
751 ),
752 },
753 []key.Binding{
754 key.NewBinding(
755 key.WithKeys("u"),
756 key.WithHelp("u", "half page up"),
757 ),
758 key.NewBinding(
759 key.WithKeys("d"),
760 key.WithHelp("d", "half page down"),
761 ),
762 key.NewBinding(
763 key.WithKeys("g", "home"),
764 key.WithHelp("g", "hone"),
765 ),
766 key.NewBinding(
767 key.WithKeys("G", "end"),
768 key.WithHelp("G", "end"),
769 ),
770 },
771 )
772 } else if a.focusedPane == PanelTypeEditor {
773 newLineBinding := key.NewBinding(
774 key.WithKeys("shift+enter", "ctrl+j"),
775 // "ctrl+j" is a common keybinding for newline in many editors. If
776 // the terminal supports "shift+enter", we substitute the help text
777 // to reflect that.
778 key.WithHelp("ctrl+j", "newline"),
779 )
780 if a.keyboardEnhancements.SupportsKeyDisambiguation() {
781 newLineBinding.SetHelp("shift+enter", newLineBinding.Help().Desc)
782 }
783 shortList = append(shortList, newLineBinding)
784 fullList = append(fullList,
785 []key.Binding{
786 newLineBinding,
787 key.NewBinding(
788 key.WithKeys("ctrl+f"),
789 key.WithHelp("ctrl+f", "add image"),
790 ),
791 key.NewBinding(
792 key.WithKeys("/"),
793 key.WithHelp("/", "add file"),
794 ),
795 key.NewBinding(
796 key.WithKeys("ctrl+v"),
797 key.WithHelp("ctrl+v", "open editor"),
798 ),
799 })
800 }
801 shortList = append(shortList,
802 // Quit
803 key.NewBinding(
804 key.WithKeys("ctrl+c"),
805 key.WithHelp("ctrl+c", "quit"),
806 ),
807 // Help
808 helpBinding,
809 )
810 fullList = append(fullList, []key.Binding{
811 key.NewBinding(
812 key.WithKeys("ctrl+g"),
813 key.WithHelp("ctrl+g", "less"),
814 ),
815 })
816 }
817
818 return core.NewSimpleHelp(shortList, fullList)
819}
820
821func (p *chatPage) IsChatFocused() bool {
822 return p.focusedPane == PanelTypeChat
823}