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