From 8daa6e774a6e02698c90392e7b2008542f789594 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Sun, 23 Mar 2025 19:19:08 +0100 Subject: add initial stuff --- internal/tui/components/repl/editor.go | 127 ++++++++++++++++++++++-- internal/tui/components/repl/messages.go | 15 ++- internal/tui/components/repl/sessions.go | 161 +++++++++++++++++++++++++++++++ internal/tui/components/repl/threads.go | 21 ---- 4 files changed, 290 insertions(+), 34 deletions(-) create mode 100644 internal/tui/components/repl/sessions.go delete mode 100644 internal/tui/components/repl/threads.go (limited to 'internal/tui/components/repl') diff --git a/internal/tui/components/repl/editor.go b/internal/tui/components/repl/editor.go index 8a04889ab..1de216bd0 100644 --- a/internal/tui/components/repl/editor.go +++ b/internal/tui/components/repl/editor.go @@ -1,21 +1,130 @@ package repl -import tea "github.com/charmbracelet/bubbletea" +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/kujtimiihoxha/termai/internal/app" + "github.com/kujtimiihoxha/termai/internal/tui/layout" + "github.com/kujtimiihoxha/vimtea" +) -type editorCmp struct{} +type EditorCmp interface { + tea.Model + layout.Focusable + layout.Sizeable + layout.Bordered +} + +type editorCmp struct { + app *app.App + editor vimtea.Editor + editorMode vimtea.EditorMode + sessionID string + focused bool + width int + height int +} + +type localKeyMap struct { + SendMessage key.Binding + SendMessageI key.Binding +} + +var keyMap = localKeyMap{ + SendMessage: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "send message normal mode"), + ), + SendMessageI: key.NewBinding( + key.WithKeys("ctrl+s"), + key.WithHelp("ctrl+s", "send message insert mode"), + ), +} + +func (m *editorCmp) Init() tea.Cmd { + return m.editor.Init() +} + +func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case vimtea.EditorModeMsg: + m.editorMode = msg.Mode + case SelectedSessionMsg: + if msg.SessionID != m.sessionID { + m.sessionID = msg.SessionID + } + } + if m.IsFocused() { + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, keyMap.SendMessage): + if m.editorMode == vimtea.ModeNormal { + return m, m.Send() + } + case key.Matches(msg, keyMap.SendMessageI): + if m.editorMode == vimtea.ModeInsert { + return m, m.Send() + } + } + } + u, cmd := m.editor.Update(msg) + m.editor = u.(vimtea.Editor) + return m, cmd + } + return m, nil +} -func (i *editorCmp) Init() tea.Cmd { +// Blur implements EditorCmp. +func (m *editorCmp) Blur() tea.Cmd { + m.focused = false return nil } -func (i *editorCmp) Update(_ tea.Msg) (tea.Model, tea.Cmd) { - return i, nil +// BorderText implements EditorCmp. +func (m *editorCmp) BorderText() map[layout.BorderPosition]string { + return map[layout.BorderPosition]string{ + layout.TopLeftBorder: "New Message", + } +} + +// Focus implements EditorCmp. +func (m *editorCmp) Focus() tea.Cmd { + m.focused = true + return m.editor.Tick() +} + +// GetSize implements EditorCmp. +func (m *editorCmp) GetSize() (int, int) { + return m.width, m.height +} + +// IsFocused implements EditorCmp. +func (m *editorCmp) IsFocused() bool { + return m.focused +} + +// SetSize implements EditorCmp. +func (m *editorCmp) SetSize(width int, height int) { + m.width = width + m.height = height + m.editor.SetSize(width, height) +} + +func (m *editorCmp) Send() tea.Cmd { + return func() tea.Msg { + // TODO: Send message + return nil + } } -func (i *editorCmp) View() string { - return "Editor" +func (m *editorCmp) View() string { + return m.editor.View() } -func NewEditorCmp() tea.Model { - return &editorCmp{} +func NewEditorCmp(app *app.App) EditorCmp { + return &editorCmp{ + app: app, + editor: vimtea.NewEditor(), + } } diff --git a/internal/tui/components/repl/messages.go b/internal/tui/components/repl/messages.go index edef26502..cd3ddac71 100644 --- a/internal/tui/components/repl/messages.go +++ b/internal/tui/components/repl/messages.go @@ -1,8 +1,13 @@ package repl -import tea "github.com/charmbracelet/bubbletea" +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/kujtimiihoxha/termai/internal/app" +) -type messagesCmp struct{} +type messagesCmp struct { + app *app.App +} func (i *messagesCmp) Init() tea.Cmd { return nil @@ -16,6 +21,8 @@ func (i *messagesCmp) View() string { return "Messages" } -func NewMessagesCmp() tea.Model { - return &messagesCmp{} +func NewMessagesCmp(app *app.App) tea.Model { + return &messagesCmp{ + app, + } } diff --git a/internal/tui/components/repl/sessions.go b/internal/tui/components/repl/sessions.go new file mode 100644 index 000000000..aef9b993e --- /dev/null +++ b/internal/tui/components/repl/sessions.go @@ -0,0 +1,161 @@ +package repl + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/kujtimiihoxha/termai/internal/app" + "github.com/kujtimiihoxha/termai/internal/session" + "github.com/kujtimiihoxha/termai/internal/tui/layout" + "github.com/kujtimiihoxha/termai/internal/tui/styles" + "github.com/kujtimiihoxha/termai/internal/tui/util" +) + +type SessionsCmp interface { + tea.Model + layout.Sizeable + layout.Focusable + layout.Bordered + layout.Bindings +} +type sessionsCmp struct { + app *app.App + list list.Model + focused bool +} + +type listItem struct { + id, title, desc string +} + +func (i listItem) Title() string { return i.title } +func (i listItem) Description() string { return i.desc } +func (i listItem) FilterValue() string { return i.title } + +type InsertSessionsMsg struct { + sessions []session.Session +} + +type SelectedSessionMsg struct { + SessionID string +} + +func (i *sessionsCmp) Init() tea.Cmd { + existing, err := i.app.Sessions.List() + if err != nil { + return util.ReportError(err) + } + if len(existing) == 0 || existing[0].MessageCount > 0 { + session, err := i.app.Sessions.Create( + "New Session", + ) + if err != nil { + return util.ReportError(err) + } + existing = append(existing, session) + } + return tea.Batch( + util.CmdHandler(InsertSessionsMsg{existing}), + util.CmdHandler(SelectedSessionMsg{existing[0].ID}), + ) +} + +func (i *sessionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case InsertSessionsMsg: + items := make([]list.Item, len(msg.sessions)) + for i, s := range msg.sessions { + items[i] = listItem{ + id: s.ID, + title: s.Title, + desc: fmt.Sprintf("Tokens: %d, Cost: %.2f", s.Tokens, s.Cost), + } + } + return i, i.list.SetItems(items) + } + if i.focused { + u, cmd := i.list.Update(msg) + i.list = u + return i, cmd + } + return i, nil +} + +func (i *sessionsCmp) View() string { + return i.list.View() +} + +func (i *sessionsCmp) Blur() tea.Cmd { + i.focused = false + return nil +} + +func (i *sessionsCmp) Focus() tea.Cmd { + i.focused = true + return nil +} + +func (i *sessionsCmp) GetSize() (int, int) { + return i.list.Width(), i.list.Height() +} + +func (i *sessionsCmp) IsFocused() bool { + return i.focused +} + +func (i *sessionsCmp) SetSize(width int, height int) { + i.list.SetSize(width, height) +} + +func (i *sessionsCmp) BorderText() map[layout.BorderPosition]string { + totalCount := len(i.list.Items()) + itemsPerPage := i.list.Paginator.PerPage + currentPage := i.list.Paginator.Page + + current := min(currentPage*itemsPerPage+itemsPerPage, totalCount) + + pageInfo := fmt.Sprintf( + "%d-%d of %d", + currentPage*itemsPerPage+1, + current, + totalCount, + ) + return map[layout.BorderPosition]string{ + layout.TopMiddleBorder: "Sessions", + layout.BottomMiddleBorder: pageInfo, + } +} + +func (i *sessionsCmp) BindingKeys() []key.Binding { + return layout.KeyMapToSlice(i.list.KeyMap) +} + +func NewSessionsCmp(app *app.App) SessionsCmp { + listDelegate := list.NewDefaultDelegate() + defaultItemStyle := list.NewDefaultItemStyles() + defaultItemStyle.SelectedTitle = defaultItemStyle.SelectedTitle.BorderForeground(styles.Secondary).Foreground(styles.Primary) + defaultItemStyle.SelectedDesc = defaultItemStyle.SelectedDesc.BorderForeground(styles.Secondary).Foreground(styles.Primary) + + defaultStyle := list.DefaultStyles() + defaultStyle.FilterPrompt = defaultStyle.FilterPrompt.Foreground(styles.Secondary) + defaultStyle.FilterCursor = defaultStyle.FilterCursor.Foreground(styles.Flamingo) + + listDelegate.Styles = defaultItemStyle + + listComponent := list.New([]list.Item{}, listDelegate, 0, 0) + listComponent.FilterInput.PromptStyle = defaultStyle.FilterPrompt + listComponent.FilterInput.Cursor.Style = defaultStyle.FilterCursor + listComponent.SetShowTitle(false) + listComponent.SetShowPagination(false) + listComponent.SetShowHelp(false) + listComponent.SetShowStatusBar(false) + listComponent.DisableQuitKeybindings() + + return &sessionsCmp{ + app: app, + list: listComponent, + focused: false, + } +} diff --git a/internal/tui/components/repl/threads.go b/internal/tui/components/repl/threads.go deleted file mode 100644 index aa2bc080b..000000000 --- a/internal/tui/components/repl/threads.go +++ /dev/null @@ -1,21 +0,0 @@ -package repl - -import tea "github.com/charmbracelet/bubbletea" - -type threadsCmp struct{} - -func (i *threadsCmp) Init() tea.Cmd { - return nil -} - -func (i *threadsCmp) Update(_ tea.Msg) (tea.Model, tea.Cmd) { - return i, nil -} - -func (i *threadsCmp) View() string { - return "Threads" -} - -func NewThreadsCmp() tea.Model { - return &threadsCmp{} -} -- cgit v1.2.3