diff options
| author | Kujtim Hoxha <[email protected]> | 2025-03-25 13:04:36 +0100 |
|---|---|---|
| committer | Kujtim Hoxha <[email protected]> | 2025-03-26 01:12:30 +0100 |
| commit | 904061c243f70696bfe781e97bf4e392e6954d07 (patch) | |
| tree | 4428f96d09968ee0cde44e6ebbaee4757f80050e /internal/tui/components | |
| parent | 005b8ac16776512b2d4b1f22bd989da162ca1bad (diff) | |
| download | opencode-904061c243f70696bfe781e97bf4e392e6954d07.tar.gz opencode-904061c243f70696bfe781e97bf4e392e6954d07.zip | |
additional tools
Diffstat (limited to 'internal/tui/components')
| -rw-r--r-- | internal/tui/components/core/button.go | 287 | ||||
| -rw-r--r-- | internal/tui/components/dialog/permission.go | 167 | ||||
| -rw-r--r-- | internal/tui/components/messages/message.go | 108 | ||||
| -rw-r--r-- | internal/tui/components/repl/editor.go | 14 | ||||
| -rw-r--r-- | internal/tui/components/repl/messages.go | 180 | ||||
| -rw-r--r-- | internal/tui/components/repl/sessions.go | 8 |
6 files changed, 737 insertions, 27 deletions
diff --git a/internal/tui/components/core/button.go b/internal/tui/components/core/button.go new file mode 100644 index 000000000..102957e0e --- /dev/null +++ b/internal/tui/components/core/button.go @@ -0,0 +1,287 @@ +package core + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/kujtimiihoxha/termai/internal/tui/styles" +) + +// ButtonKeyMap defines key bindings for the button component +type ButtonKeyMap struct { + Enter key.Binding +} + +// DefaultButtonKeyMap returns default key bindings for the button +func DefaultButtonKeyMap() ButtonKeyMap { + return ButtonKeyMap{ + Enter: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "select"), + ), + } +} + +// ShortHelp returns keybinding help +func (k ButtonKeyMap) ShortHelp() []key.Binding { + return []key.Binding{k.Enter} +} + +// FullHelp returns full help info for keybindings +func (k ButtonKeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {k.Enter}, + } +} + +// ButtonState represents the state of a button +type ButtonState int + +const ( + // ButtonNormal is the default state + ButtonNormal ButtonState = iota + // ButtonHovered is when the button is focused/hovered + ButtonHovered + // ButtonPressed is when the button is being pressed + ButtonPressed + // ButtonDisabled is when the button is disabled + ButtonDisabled +) + +// ButtonVariant defines the visual style variant of a button +type ButtonVariant int + +const ( + // ButtonPrimary uses primary color styling + ButtonPrimary ButtonVariant = iota + // ButtonSecondary uses secondary color styling + ButtonSecondary + // ButtonDanger uses danger/error color styling + ButtonDanger + // ButtonWarning uses warning color styling + ButtonWarning + // ButtonNeutral uses neutral color styling + ButtonNeutral +) + +// ButtonMsg is sent when a button is clicked +type ButtonMsg struct { + ID string + Payload interface{} +} + +// ButtonCmp represents a clickable button component +type ButtonCmp struct { + id string + label string + width int + height int + state ButtonState + variant ButtonVariant + keyMap ButtonKeyMap + payload interface{} + style lipgloss.Style + hoverStyle lipgloss.Style +} + +// NewButtonCmp creates a new button component +func NewButtonCmp(id, label string) *ButtonCmp { + b := &ButtonCmp{ + id: id, + label: label, + state: ButtonNormal, + variant: ButtonPrimary, + keyMap: DefaultButtonKeyMap(), + width: len(label) + 4, // add some padding + height: 1, + } + b.updateStyles() + return b +} + +// WithVariant sets the button variant +func (b *ButtonCmp) WithVariant(variant ButtonVariant) *ButtonCmp { + b.variant = variant + b.updateStyles() + return b +} + +// WithPayload sets the payload sent with button events +func (b *ButtonCmp) WithPayload(payload interface{}) *ButtonCmp { + b.payload = payload + return b +} + +// WithWidth sets a custom width +func (b *ButtonCmp) WithWidth(width int) *ButtonCmp { + b.width = width + b.updateStyles() + return b +} + +// updateStyles recalculates styles based on current state and variant +func (b *ButtonCmp) updateStyles() { + // Base styles + b.style = styles.Regular. + Padding(0, 1). + Width(b.width). + Align(lipgloss.Center). + BorderStyle(lipgloss.RoundedBorder()) + + b.hoverStyle = b.style. + Bold(true) + + // Variant-specific styling + switch b.variant { + case ButtonPrimary: + b.style = b.style. + Foreground(styles.Base). + Background(styles.Primary). + BorderForeground(styles.Primary) + + b.hoverStyle = b.hoverStyle. + Foreground(styles.Base). + Background(styles.Blue). + BorderForeground(styles.Blue) + + case ButtonSecondary: + b.style = b.style. + Foreground(styles.Base). + Background(styles.Secondary). + BorderForeground(styles.Secondary) + + b.hoverStyle = b.hoverStyle. + Foreground(styles.Base). + Background(styles.Mauve). + BorderForeground(styles.Mauve) + + case ButtonDanger: + b.style = b.style. + Foreground(styles.Base). + Background(styles.Error). + BorderForeground(styles.Error) + + b.hoverStyle = b.hoverStyle. + Foreground(styles.Base). + Background(styles.Red). + BorderForeground(styles.Red) + + case ButtonWarning: + b.style = b.style. + Foreground(styles.Text). + Background(styles.Warning). + BorderForeground(styles.Warning) + + b.hoverStyle = b.hoverStyle. + Foreground(styles.Text). + Background(styles.Peach). + BorderForeground(styles.Peach) + + case ButtonNeutral: + b.style = b.style. + Foreground(styles.Text). + Background(styles.Grey). + BorderForeground(styles.Grey) + + b.hoverStyle = b.hoverStyle. + Foreground(styles.Text). + Background(styles.DarkGrey). + BorderForeground(styles.DarkGrey) + } + + // Disabled style override + if b.state == ButtonDisabled { + b.style = b.style. + Foreground(styles.SubText0). + Background(styles.LightGrey). + BorderForeground(styles.LightGrey) + } +} + +// SetSize sets the button size +func (b *ButtonCmp) SetSize(width, height int) { + b.width = width + b.height = height + b.updateStyles() +} + +// Focus sets the button to focused state +func (b *ButtonCmp) Focus() tea.Cmd { + if b.state != ButtonDisabled { + b.state = ButtonHovered + } + return nil +} + +// Blur sets the button to normal state +func (b *ButtonCmp) Blur() tea.Cmd { + if b.state != ButtonDisabled { + b.state = ButtonNormal + } + return nil +} + +// Disable sets the button to disabled state +func (b *ButtonCmp) Disable() { + b.state = ButtonDisabled + b.updateStyles() +} + +// Enable enables the button if disabled +func (b *ButtonCmp) Enable() { + if b.state == ButtonDisabled { + b.state = ButtonNormal + b.updateStyles() + } +} + +// IsDisabled returns whether the button is disabled +func (b *ButtonCmp) IsDisabled() bool { + return b.state == ButtonDisabled +} + +// IsFocused returns whether the button is focused +func (b *ButtonCmp) IsFocused() bool { + return b.state == ButtonHovered +} + +// Init initializes the button +func (b *ButtonCmp) Init() tea.Cmd { + return nil +} + +// Update handles messages and user input +func (b *ButtonCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // Skip updates if disabled + if b.state == ButtonDisabled { + return b, nil + } + + switch msg := msg.(type) { + case tea.KeyMsg: + // Handle key presses when focused + if b.state == ButtonHovered { + switch { + case key.Matches(msg, b.keyMap.Enter): + b.state = ButtonPressed + return b, func() tea.Msg { + return ButtonMsg{ + ID: b.id, + Payload: b.payload, + } + } + } + } + } + + return b, nil +} + +// View renders the button +func (b *ButtonCmp) View() string { + if b.state == ButtonHovered || b.state == ButtonPressed { + return b.hoverStyle.Render(b.label) + } + return b.style.Render(b.label) +} + diff --git a/internal/tui/components/dialog/permission.go b/internal/tui/components/dialog/permission.go new file mode 100644 index 000000000..f7581330a --- /dev/null +++ b/internal/tui/components/dialog/permission.go @@ -0,0 +1,167 @@ +package dialog + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/kujtimiihoxha/termai/internal/permission" + "github.com/kujtimiihoxha/termai/internal/tui/components/core" + "github.com/kujtimiihoxha/termai/internal/tui/layout" + "github.com/kujtimiihoxha/termai/internal/tui/styles" + "github.com/kujtimiihoxha/termai/internal/tui/util" + + "github.com/charmbracelet/huh" +) + +type PermissionAction string + +// Permission responses +const ( + PermissionAllow PermissionAction = "allow" + PermissionAllowForSession PermissionAction = "allow_session" + PermissionDeny PermissionAction = "deny" +) + +// PermissionResponseMsg represents the user's response to a permission request +type PermissionResponseMsg struct { + Permission permission.PermissionRequest + Action PermissionAction +} + +// Width and height constants for the dialog +var ( + permissionWidth = 60 + permissionHeight = 10 +) + +// PermissionDialog interface for permission dialog component +type PermissionDialog interface { + tea.Model + layout.Sizeable + layout.Bindings +} + +// permissionDialogCmp is the implementation of PermissionDialog +type permissionDialogCmp struct { + form *huh.Form + content string + width int + height int + permission permission.PermissionRequest +} + +func (p *permissionDialogCmp) Init() tea.Cmd { + return nil +} + +func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + // Process the form + form, cmd := p.form.Update(msg) + if f, ok := form.(*huh.Form); ok { + p.form = f + cmds = append(cmds, cmd) + } + + if p.form.State == huh.StateCompleted { + // Get the selected action + action := p.form.GetString("action") + + // Close the dialog and return the response + return p, tea.Batch( + util.CmdHandler(core.DialogCloseMsg{}), + util.CmdHandler(PermissionResponseMsg{Action: PermissionAction(action), Permission: p.permission}), + ) + } + + return p, tea.Batch(cmds...) +} + +func (p *permissionDialogCmp) View() string { + contentStyle := lipgloss.NewStyle(). + Width(p.width). + Padding(1, 0). + Foreground(styles.Text). + Align(lipgloss.Center) + + return lipgloss.JoinVertical( + lipgloss.Center, + contentStyle.Render(p.content), + p.form.View(), + ) +} + +func (p *permissionDialogCmp) GetSize() (int, int) { + return p.width, p.height +} + +func (p *permissionDialogCmp) SetSize(width int, height int) { + p.width = width + p.height = height +} + +func (p *permissionDialogCmp) BindingKeys() []key.Binding { + return p.form.KeyBinds() +} + +func newPermissionDialogCmp(permission permission.PermissionRequest, content string) PermissionDialog { + // Create a note field for displaying the content + + // Create select field for the permission options + selectOption := huh.NewSelect[string](). + Key("action"). + Options( + huh.NewOption("Allow", string(PermissionAllow)), + huh.NewOption("Allow for this session", string(PermissionAllowForSession)), + huh.NewOption("Deny", string(PermissionDeny)), + ). + Title("Permission Request") + + // Apply theme + theme := styles.HuhTheme() + + // Setup form width and height + form := huh.NewForm(huh.NewGroup(selectOption)). + WithWidth(permissionWidth - 2). + WithShowHelp(false). + WithTheme(theme). + WithShowErrors(false) + + // Focus the form for immediate interaction + selectOption.Focus() + + return &permissionDialogCmp{ + permission: permission, + form: form, + content: content, + width: permissionWidth, + height: permissionHeight, + } +} + +// NewPermissionDialogCmd creates a new permission dialog command +func NewPermissionDialogCmd(permission permission.PermissionRequest, content string) tea.Cmd { + permDialog := newPermissionDialogCmp(permission, content) + + // Create the dialog layout + dialogPane := layout.NewSinglePane( + permDialog.(*permissionDialogCmp), + layout.WithSignlePaneSize(permissionWidth+2, permissionHeight+2), + layout.WithSinglePaneBordered(true), + layout.WithSinglePaneFocusable(true), + layout.WithSinglePaneActiveColor(styles.Blue), + layout.WithSignlePaneBorderText(map[layout.BorderPosition]string{ + layout.TopMiddleBorder: " Permission Required ", + }), + ) + + // Focus the dialog + dialogPane.Focus() + + // Return the dialog command + return util.CmdHandler(core.DialogMsg{ + Content: dialogPane, + }) +} + diff --git a/internal/tui/components/messages/message.go b/internal/tui/components/messages/message.go new file mode 100644 index 000000000..619950850 --- /dev/null +++ b/internal/tui/components/messages/message.go @@ -0,0 +1,108 @@ +package messages + +import ( + "fmt" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/cloudwego/eino/schema" + "github.com/kujtimiihoxha/termai/internal/message" + "github.com/kujtimiihoxha/termai/internal/tui/layout" + "github.com/kujtimiihoxha/termai/internal/tui/styles" +) + +const ( + maxHeight = 10 +) + +type MessagesCmp interface { + tea.Model + layout.Focusable + layout.Bordered + layout.Sizeable +} + +type messageCmp struct { + message message.Message + width int + height int + focused bool + expanded bool +} + +func (m *messageCmp) Init() tea.Cmd { + return nil +} + +func (m *messageCmp) Update(tea.Msg) (tea.Model, tea.Cmd) { + return m, nil +} + +func (m *messageCmp) View() string { + wrapper := layout.NewSinglePane( + m, + layout.WithSinglePaneBordered(true), + layout.WithSinglePaneFocusable(true), + layout.WithSinglePanePadding(1), + layout.WithSinglePaneActiveColor(m.borderColor()), + ) + if m.focused { + wrapper.Focus() + } + wrapper.SetSize(m.width, m.height) + return wrapper.View() +} + +func (m *messageCmp) Blur() tea.Cmd { + m.focused = false + return nil +} + +func (m *messageCmp) borderColor() lipgloss.TerminalColor { + switch m.message.MessageData.Role { + case schema.Assistant: + return styles.Mauve + case schema.User: + return styles.Flamingo + } + return styles.Blue +} + +func (m *messageCmp) BorderText() map[layout.BorderPosition]string { + role := "" + icon := "" + switch m.message.MessageData.Role { + case schema.Assistant: + role = "Assistant" + icon = styles.BotIcon + case schema.User: + role = "User" + icon = styles.UserIcon + } + return map[layout.BorderPosition]string{ + layout.TopLeftBorder: fmt.Sprintf("%s %s ", role, icon), + } +} + +func (m *messageCmp) Focus() tea.Cmd { + m.focused = true + return nil +} + +func (m *messageCmp) IsFocused() bool { + return m.focused +} + +func (m *messageCmp) GetSize() (int, int) { + return m.width, 0 +} + +func (m *messageCmp) SetSize(width int, height int) { + m.width = width +} + +func NewMessageCmp(msg message.Message) MessagesCmp { + return &messageCmp{ + message: msg, + } +} diff --git a/internal/tui/components/repl/editor.go b/internal/tui/components/repl/editor.go index 8d795eb14..d0af8d2c5 100644 --- a/internal/tui/components/repl/editor.go +++ b/internal/tui/components/repl/editor.go @@ -5,9 +5,11 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/cloudwego/eino/schema" "github.com/kujtimiihoxha/termai/internal/app" "github.com/kujtimiihoxha/termai/internal/tui/layout" + "github.com/kujtimiihoxha/termai/internal/tui/styles" "github.com/kujtimiihoxha/vimtea" ) @@ -105,8 +107,12 @@ func (m *editorCmp) Blur() tea.Cmd { } func (m *editorCmp) BorderText() map[layout.BorderPosition]string { + title := "New Message" + if m.focused { + title = lipgloss.NewStyle().Foreground(styles.Primary).Render(title) + } return map[layout.BorderPosition]string{ - layout.TopLeftBorder: "New Message", + layout.TopLeftBorder: title, } } @@ -148,7 +154,9 @@ func (m *editorCmp) BindingKeys() []key.Binding { func NewEditorCmp(app *app.App) EditorCmp { return &editorCmp{ - app: app, - editor: vimtea.NewEditor(), + app: app, + editor: vimtea.NewEditor( + vimtea.WithFileName("message.md"), + ), } } diff --git a/internal/tui/components/repl/messages.go b/internal/tui/components/repl/messages.go index feddf7bf6..9b3c5bde8 100644 --- a/internal/tui/components/repl/messages.go +++ b/internal/tui/components/repl/messages.go @@ -1,15 +1,22 @@ package repl import ( + "fmt" + "slices" + "strings" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" + "github.com/cloudwego/eino/schema" "github.com/kujtimiihoxha/termai/internal/app" "github.com/kujtimiihoxha/termai/internal/message" "github.com/kujtimiihoxha/termai/internal/pubsub" "github.com/kujtimiihoxha/termai/internal/session" "github.com/kujtimiihoxha/termai/internal/tui/layout" + "github.com/kujtimiihoxha/termai/internal/tui/styles" ) type MessagesCmp interface { @@ -21,13 +28,15 @@ type MessagesCmp interface { } type messagesCmp struct { - app *app.App - messages []message.Message - session session.Session - viewport viewport.Model - width int - height int - focused bool + app *app.App + messages []message.Message + session session.Session + viewport viewport.Model + mdRenderer *glamour.TermRenderer + width int + height int + focused bool + cachedView string } func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -35,6 +44,8 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case pubsub.Event[message.Message]: if msg.Type == pubsub.CreatedEvent { m.messages = append(m.messages, msg.Payload) + m.renderView() + m.viewport.GotoBottom() } case pubsub.Event[session.Session]: if msg.Type == pubsub.UpdatedEvent { @@ -45,60 +56,182 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case SelectedSessionMsg: m.session, _ = m.app.Sessions.Get(msg.SessionID) m.messages, _ = m.app.Messages.List(m.session.ID) + m.renderView() + m.viewport.GotoBottom() + } + if m.focused { + u, cmd := m.viewport.Update(msg) + m.viewport = u + return m, cmd } return m, nil } -func (i *messagesCmp) View() string { - stringMessages := make([]string, len(i.messages)) - for idx, msg := range i.messages { - stringMessages[idx] = msg.MessageData.Content +func borderColor(role schema.RoleType) lipgloss.TerminalColor { + switch role { + case schema.Assistant: + return styles.Mauve + case schema.User: + return styles.Rosewater + case schema.Tool: + return styles.Peach } - return lipgloss.JoinVertical(lipgloss.Top, stringMessages...) + return styles.Blue +} + +func borderText(msgRole schema.RoleType, currentMessage int) map[layout.BorderPosition]string { + role := "" + icon := "" + switch msgRole { + case schema.Assistant: + role = "Assistant" + icon = styles.BotIcon + case schema.User: + role = "User" + icon = styles.UserIcon + } + return map[layout.BorderPosition]string{ + layout.TopLeftBorder: lipgloss.NewStyle(). + Padding(0, 1). + Bold(true). + Foreground(styles.Crust). + Background(borderColor(msgRole)). + Render(fmt.Sprintf("%s %s ", role, icon)), + layout.TopRightBorder: lipgloss.NewStyle(). + Padding(0, 1). + Bold(true). + Foreground(styles.Crust). + Background(borderColor(msgRole)). + Render(fmt.Sprintf("#%d ", currentMessage)), + } +} + +func (m *messagesCmp) renderView() { + stringMessages := make([]string, 0) + r, _ := glamour.NewTermRenderer( + glamour.WithStyles(styles.CatppuccinMarkdownStyle()), + glamour.WithWordWrap(m.width-10), + glamour.WithEmoji(), + ) + textStyle := lipgloss.NewStyle().Width(m.width - 4) + currentMessage := 1 + for _, msg := range m.messages { + if msg.MessageData.Role == schema.Tool { + continue + } + content := msg.MessageData.Content + if content != "" { + content, _ = r.Render(msg.MessageData.Content) + stringMessages = append(stringMessages, layout.Borderize( + textStyle.Render(content), + layout.BorderOptions{ + InactiveBorder: lipgloss.DoubleBorder(), + ActiveBorder: lipgloss.DoubleBorder(), + ActiveColor: borderColor(msg.MessageData.Role), + InactiveColor: borderColor(msg.MessageData.Role), + EmbeddedText: borderText(msg.MessageData.Role, currentMessage), + }, + )) + currentMessage++ + } + for _, toolCall := range msg.MessageData.ToolCalls { + resultInx := slices.IndexFunc(m.messages, func(m message.Message) bool { + return m.MessageData.ToolCallID == toolCall.ID + }) + content := fmt.Sprintf("**Arguments**\n```json\n%s\n```\n", toolCall.Function.Arguments) + if resultInx == -1 { + content += "Running..." + } else { + result := m.messages[resultInx].MessageData.Content + if result != "" { + lines := strings.Split(result, "\n") + if len(lines) > 15 { + result = strings.Join(lines[:15], "\n") + } + content += fmt.Sprintf("**Result**\n```\n%s\n```\n", result) + if len(lines) > 15 { + content += fmt.Sprintf("\n\n *...%d lines are truncated* ", len(lines)-15) + } + } + } + content, _ = r.Render(content) + stringMessages = append(stringMessages, layout.Borderize( + textStyle.Render(content), + layout.BorderOptions{ + InactiveBorder: lipgloss.DoubleBorder(), + ActiveBorder: lipgloss.DoubleBorder(), + ActiveColor: borderColor(schema.Tool), + InactiveColor: borderColor(schema.Tool), + EmbeddedText: map[layout.BorderPosition]string{ + layout.TopLeftBorder: lipgloss.NewStyle(). + Padding(0, 1). + Bold(true). + Foreground(styles.Crust). + Background(borderColor(schema.Tool)). + Render( + fmt.Sprintf("Tool [%s] %s ", toolCall.Function.Name, styles.ToolIcon), + ), + layout.TopRightBorder: lipgloss.NewStyle(). + Padding(0, 1). + Bold(true). + Foreground(styles.Crust). + Background(borderColor(schema.Tool)). + Render(fmt.Sprintf("#%d ", currentMessage)), + }, + }, + )) + currentMessage++ + } + } + m.viewport.SetContent(lipgloss.JoinVertical(lipgloss.Top, stringMessages...)) +} + +func (m *messagesCmp) View() string { + return lipgloss.NewStyle().Padding(1).Render(m.viewport.View()) } -// BindingKeys implements MessagesCmp. func (m *messagesCmp) BindingKeys() []key.Binding { - return []key.Binding{} + return layout.KeyMapToSlice(m.viewport.KeyMap) } -// Blur implements MessagesCmp. func (m *messagesCmp) Blur() tea.Cmd { m.focused = false return nil } -// BorderText implements MessagesCmp. func (m *messagesCmp) BorderText() map[layout.BorderPosition]string { title := m.session.Title - if len(title) > 20 { - title = title[:20] + "..." + titleWidth := m.width / 2 + if len(title) > titleWidth { + title = title[:titleWidth] + "..." + } + if m.focused { + title = lipgloss.NewStyle().Foreground(styles.Primary).Render(title) } return map[layout.BorderPosition]string{ - layout.TopLeftBorder: title, + layout.TopLeftBorder: title, + layout.BottomRightBorder: formatTokensAndCost(m.session.CompletionTokens+m.session.PromptTokens, m.session.Cost), } } -// Focus implements MessagesCmp. func (m *messagesCmp) Focus() tea.Cmd { m.focused = true return nil } -// GetSize implements MessagesCmp. func (m *messagesCmp) GetSize() (int, int) { return m.width, m.height } -// IsFocused implements MessagesCmp. func (m *messagesCmp) IsFocused() bool { return m.focused } -// SetSize implements MessagesCmp. func (m *messagesCmp) SetSize(width int, height int) { m.width = width m.height = height + m.viewport.Width = width - 2 // padding + m.viewport.Height = height - 2 // padding } func (m *messagesCmp) Init() tea.Cmd { @@ -109,5 +242,6 @@ func NewMessagesCmp(app *app.App) MessagesCmp { return &messagesCmp{ app: app, messages: []message.Message{}, + viewport: viewport.New(0, 0), } } diff --git a/internal/tui/components/repl/sessions.go b/internal/tui/components/repl/sessions.go index 5d2411fb6..0f208ced9 100644 --- a/internal/tui/components/repl/sessions.go +++ b/internal/tui/components/repl/sessions.go @@ -7,6 +7,7 @@ import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/kujtimiihoxha/termai/internal/app" "github.com/kujtimiihoxha/termai/internal/pubsub" "github.com/kujtimiihoxha/termai/internal/session" @@ -160,8 +161,13 @@ func (i *sessionsCmp) BorderText() map[layout.BorderPosition]string { current, totalCount, ) + + title := "Sessions" + if i.focused { + title = lipgloss.NewStyle().Foreground(styles.Primary).Render(title) + } return map[layout.BorderPosition]string{ - layout.TopMiddleBorder: "Sessions", + layout.TopMiddleBorder: title, layout.BottomMiddleBorder: pageInfo, } } |
