diff options
| author | Kujtim Hoxha <[email protected]> | 2025-04-09 19:07:39 +0200 |
|---|---|---|
| committer | Kujtim Hoxha <[email protected]> | 2025-04-09 19:07:39 +0200 |
| commit | d39d52d95d6aaab67fb3a17efb9ed62cc290e72f (patch) | |
| tree | 940bad6f0c847ceb213e5fc684b6d87cbf9d6996 /internal/tui | |
| parent | 0d8d324ac6e640b95f4f2f62fd189399a959319a (diff) | |
| download | opencode-d39d52d95d6aaab67fb3a17efb9ed62cc290e72f.tar.gz opencode-d39d52d95d6aaab67fb3a17efb9ed62cc290e72f.zip | |
finish logs page
Diffstat (limited to 'internal/tui')
| -rw-r--r-- | internal/tui/components/core/status.go | 24 | ||||
| -rw-r--r-- | internal/tui/components/logs/details.go | 176 | ||||
| -rw-r--r-- | internal/tui/components/logs/table.go | 38 | ||||
| -rw-r--r-- | internal/tui/layout/bento.go | 55 | ||||
| -rw-r--r-- | internal/tui/page/logs.go | 19 | ||||
| -rw-r--r-- | internal/tui/tui.go | 84 | ||||
| -rw-r--r-- | internal/tui/util/util.go | 7 |
7 files changed, 331 insertions, 72 deletions
diff --git a/internal/tui/components/core/status.go b/internal/tui/components/core/status.go index 83ef07fe3..6c4dda0a4 100644 --- a/internal/tui/components/core/status.go +++ b/internal/tui/components/core/status.go @@ -7,7 +7,6 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/kujtimiihoxha/termai/internal/config" "github.com/kujtimiihoxha/termai/internal/llm/models" - "github.com/kujtimiihoxha/termai/internal/pubsub" "github.com/kujtimiihoxha/termai/internal/tui/styles" "github.com/kujtimiihoxha/termai/internal/tui/util" "github.com/kujtimiihoxha/termai/internal/version" @@ -20,8 +19,8 @@ type statusCmp struct { } // clearMessageCmd is a command that clears status messages after a timeout -func (m statusCmp) clearMessageCmd() tea.Cmd { - return tea.Tick(m.messageTTL, func(time.Time) tea.Msg { +func (m statusCmp) clearMessageCmd(ttl time.Duration) tea.Cmd { + return tea.Tick(ttl, func(time.Time) tea.Msg { return util.ClearStatusMsg{} }) } @@ -34,13 +33,14 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width - return m, m.clearMessageCmd() - case pubsub.Event[util.InfoMsg]: - m.info = &msg.Payload - return m, m.clearMessageCmd() + return m, nil case util.InfoMsg: m.info = &msg - return m, m.clearMessageCmd() + ttl := msg.TTL + if ttl == 0 { + ttl = m.messageTTL + } + return m, m.clearMessageCmd(ttl) case util.ClearStatusMsg: m.info = nil } @@ -66,7 +66,13 @@ func (m statusCmp) View() string { case util.InfoTypeError: infoStyle = infoStyle.Background(styles.Red) } - status += infoStyle.Render(m.info.Msg) + // Truncate message if it's longer than available width + msg := m.info.Msg + availWidth := m.availableFooterMsgWidth() - 3 // Account for ellipsis + if len(msg) > availWidth && availWidth > 0 { + msg = msg[:availWidth] + "..." + } + status += infoStyle.Render(msg) } else { status += styles.Padded. Foreground(styles.Base). diff --git a/internal/tui/components/logs/details.go b/internal/tui/components/logs/details.go new file mode 100644 index 000000000..e02dfd9cc --- /dev/null +++ b/internal/tui/components/logs/details.go @@ -0,0 +1,176 @@ +package logs + +import ( + "fmt" + "strings" + "time" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/kujtimiihoxha/termai/internal/logging" + "github.com/kujtimiihoxha/termai/internal/tui/layout" + "github.com/kujtimiihoxha/termai/internal/tui/styles" +) + +type DetailComponent interface { + tea.Model + layout.Focusable + layout.Sizeable + layout.Bindings + layout.Bordered +} + +type detailCmp struct { + width, height int + focused bool + currentLog logging.LogMessage + viewport viewport.Model +} + +func (i *detailCmp) Init() tea.Cmd { + messages := logging.Get().List() + if len(messages) == 0 { + return nil + } + i.currentLog = messages[0] + return nil +} + +func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var ( + cmd tea.Cmd + cmds []tea.Cmd + ) + + switch msg := msg.(type) { + case selectedLogMsg: + if msg.ID != i.currentLog.ID { + i.currentLog = logging.LogMessage(msg) + i.updateContent() + } + } + + if i.focused { + i.viewport, cmd = i.viewport.Update(msg) + cmds = append(cmds, cmd) + } + + return i, tea.Batch(cmds...) +} + +func (i *detailCmp) updateContent() { + var content strings.Builder + + // Format the header with timestamp and level + timeStyle := lipgloss.NewStyle().Foreground(styles.SubText0) + levelStyle := getLevelStyle(i.currentLog.Level) + + header := lipgloss.JoinHorizontal( + lipgloss.Center, + timeStyle.Render(i.currentLog.Time.Format(time.RFC3339)), + " ", + levelStyle.Render(i.currentLog.Level), + ) + + content.WriteString(lipgloss.NewStyle().Bold(true).Render(header)) + content.WriteString("\n\n") + + // Message with styling + messageStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text) + content.WriteString(messageStyle.Render("Message:")) + content.WriteString("\n") + content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(i.currentLog.Message)) + content.WriteString("\n\n") + + // Attributes section + if len(i.currentLog.Attributes) > 0 { + attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text) + content.WriteString(attrHeaderStyle.Render("Attributes:")) + content.WriteString("\n") + + // Create a table-like display for attributes + keyStyle := lipgloss.NewStyle().Foreground(styles.Primary).Bold(true) + valueStyle := lipgloss.NewStyle().Foreground(styles.Text) + + for _, attr := range i.currentLog.Attributes { + attrLine := fmt.Sprintf("%s: %s", + keyStyle.Render(attr.Key), + valueStyle.Render(attr.Value), + ) + content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(attrLine)) + content.WriteString("\n") + } + } + + i.viewport.SetContent(content.String()) +} + +func getLevelStyle(level string) lipgloss.Style { + style := lipgloss.NewStyle().Bold(true) + + switch strings.ToLower(level) { + case "info": + return style.Foreground(styles.Blue) + case "warn", "warning": + return style.Foreground(styles.Warning) + case "error", "err": + return style.Foreground(styles.Error) + case "debug": + return style.Foreground(styles.Green) + default: + return style.Foreground(styles.Text) + } +} + +func (i *detailCmp) View() string { + return i.viewport.View() +} + +func (i *detailCmp) Blur() tea.Cmd { + i.focused = false + return nil +} + +func (i *detailCmp) Focus() tea.Cmd { + i.focused = true + return nil +} + +func (i *detailCmp) IsFocused() bool { + return i.focused +} + +func (i *detailCmp) GetSize() (int, int) { + return i.width, i.height +} + +func (i *detailCmp) SetSize(width int, height int) { + i.width = width + i.height = height + i.viewport.Width = i.width + i.viewport.Height = i.height + i.updateContent() +} + +func (i *detailCmp) BindingKeys() []key.Binding { + return []key.Binding{ + i.viewport.KeyMap.PageDown, + i.viewport.KeyMap.PageUp, + i.viewport.KeyMap.HalfPageDown, + i.viewport.KeyMap.HalfPageUp, + } +} + +func (i *detailCmp) BorderText() map[layout.BorderPosition]string { + return map[layout.BorderPosition]string{ + layout.TopLeftBorder: "Log Details", + } +} + +func NewLogsDetails() DetailComponent { + return &detailCmp{ + viewport: viewport.New(0, 0), + } +} diff --git a/internal/tui/components/logs/table.go b/internal/tui/components/logs/table.go index c268fc686..e84b4149c 100644 --- a/internal/tui/components/logs/table.go +++ b/internal/tui/components/logs/table.go @@ -11,6 +11,7 @@ import ( "github.com/kujtimiihoxha/termai/internal/pubsub" "github.com/kujtimiihoxha/termai/internal/tui/layout" "github.com/kujtimiihoxha/termai/internal/tui/styles" + "github.com/kujtimiihoxha/termai/internal/tui/util" ) type TableComponent interface { @@ -26,29 +27,42 @@ type tableCmp struct { table table.Model } +type selectedLogMsg logging.LogMessage + func (i *tableCmp) Init() tea.Cmd { i.setRows() return nil } func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd if i.table.Focused() { - switch msg := msg.(type) { - case pubsub.Event[logging.Message]: + switch msg.(type) { + case pubsub.Event[logging.LogMessage]: i.setRows() return i, nil - case tea.KeyMsg: - if msg.String() == "ctrl+s" { - logger.Info("Saving logs...", - "rows", len(i.table.Rows()), - ) - } } + prevSelectedRow := i.table.SelectedRow() t, cmd := i.table.Update(msg) + cmds = append(cmds, cmd) i.table = t - return i, cmd + selectedRow := i.table.SelectedRow() + if selectedRow != nil { + if prevSelectedRow == nil || selectedRow[0] == prevSelectedRow[0] { + var log logging.LogMessage + for _, row := range logging.Get().List() { + if row.ID == selectedRow[0] { + log = row + break + } + } + if log.ID != "" { + cmds = append(cmds, util.CmdHandler(selectedLogMsg(log))) + } + } + } } - return i, nil + return i, tea.Batch(cmds...) } func (i *tableCmp) View() string { @@ -92,7 +106,7 @@ func (i *tableCmp) setRows() { rows := []table.Row{} logs := logger.List() - slices.SortFunc(logs, func(a, b logging.Message) int { + slices.SortFunc(logs, func(a, b logging.LogMessage) int { if a.Time.Before(b.Time) { return 1 } @@ -106,6 +120,7 @@ func (i *tableCmp) setRows() { bm, _ := json.Marshal(log.Attributes) row := table.Row{ + log.ID, log.Time.Format("15:04:05"), log.Level, log.Message, @@ -118,6 +133,7 @@ func (i *tableCmp) setRows() { func NewLogsTable() TableComponent { columns := []table.Column{ + {Title: "ID", Width: 4}, {Title: "Time", Width: 4}, {Title: "Level", Width: 10}, {Title: "Message", Width: 10}, diff --git a/internal/tui/layout/bento.go b/internal/tui/layout/bento.go index 5225db192..c47c4e090 100644 --- a/internal/tui/layout/bento.go +++ b/internal/tui/layout/bento.go @@ -187,7 +187,6 @@ func (b *bentoLayout) SetSize(width int, height int) { b.width = width b.height = height - // Check which panes are available leftExists := false rightTopExists := false rightBottomExists := false @@ -218,7 +217,6 @@ func (b *bentoLayout) SetSize(width int, height int) { rightTopHeight = int(float64(height) * b.rightTopHeightRatio) rightBottomHeight = height - rightTopHeight - // Ensure minimum height for bottom pane if rightBottomHeight < minRightBottomHeight && height >= minRightBottomHeight { rightBottomHeight = minRightBottomHeight rightTopHeight = height - rightBottomHeight @@ -271,23 +269,47 @@ func (b *bentoLayout) HidePane(pane paneID) tea.Cmd { } func (b *bentoLayout) SwitchPane(back bool) tea.Cmd { + orderForward := []paneID{BentoLeftPane, BentoRightTopPane, BentoRightBottomPane} + orderBackward := []paneID{BentoLeftPane, BentoRightBottomPane, BentoRightTopPane} + + order := orderForward if back { - switch b.currentPane { - case BentoLeftPane: - b.currentPane = BentoRightBottomPane - case BentoRightTopPane: - b.currentPane = BentoLeftPane - case BentoRightBottomPane: - b.currentPane = BentoRightTopPane + order = orderBackward + } + + currentIdx := -1 + for i, id := range order { + if id == b.currentPane { + currentIdx = i + break + } + } + + if currentIdx == -1 { + for _, id := range order { + if _, exists := b.panes[id]; exists { + if _, hidden := b.hiddenPanes[id]; !hidden { + b.currentPane = id + break + } + } } } else { - switch b.currentPane { - case BentoLeftPane: - b.currentPane = BentoRightTopPane - case BentoRightTopPane: - b.currentPane = BentoRightBottomPane - case BentoRightBottomPane: - b.currentPane = BentoLeftPane + startIdx := currentIdx + for { + currentIdx = (currentIdx + 1) % len(order) + + nextID := order[currentIdx] + if _, exists := b.panes[nextID]; exists { + if _, hidden := b.hiddenPanes[nextID]; !hidden { + b.currentPane = nextID + break + } + } + + if currentIdx == startIdx { + break + } } } @@ -319,7 +341,6 @@ type BentoLayoutOption func(*bentoLayout) func NewBentoLayout(panes BentoPanes, opts ...BentoLayoutOption) BentoLayout { p := make(map[paneID]SinglePaneLayout, len(panes)) for id, pane := range panes { - // Wrap any pane that is not a SinglePaneLayout in a SinglePaneLayout if sp, ok := pane.(SinglePaneLayout); !ok { p[id] = NewSinglePane( pane, diff --git a/internal/tui/page/logs.go b/internal/tui/page/logs.go index 17b1f2340..12afaf6aa 100644 --- a/internal/tui/page/logs.go +++ b/internal/tui/page/logs.go @@ -9,17 +9,12 @@ import ( var LogsPage PageID = "logs" func NewLogsPage() tea.Model { - p := layout.NewSinglePane( - logs.NewLogsTable(), - layout.WithSinglePaneFocusable(true), - layout.WithSinglePaneBordered(true), - layout.WithSignlePaneBorderText( - map[layout.BorderPosition]string{ - layout.TopMiddleBorder: "Logs", - }, - ), - layout.WithSinglePanePadding(1), + return layout.NewBentoLayout( + layout.BentoPanes{ + layout.BentoRightTopPane: logs.NewLogsTable(), + layout.BentoRightBottomPane: logs.NewLogsDetails(), + }, + layout.WithBentoLayoutCurrentPane(layout.BentoRightTopPane), + layout.WithBentoLayoutRightTopHeightRatio(0.5), ) - p.Focus() - return p } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 314e0f17a..c94a6b020 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -5,6 +5,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/kujtimiihoxha/termai/internal/app" + "github.com/kujtimiihoxha/termai/internal/logging" "github.com/kujtimiihoxha/termai/internal/permission" "github.com/kujtimiihoxha/termai/internal/pubsub" "github.com/kujtimiihoxha/termai/internal/tui/components/core" @@ -74,22 +75,9 @@ func (a appModel) Init() tea.Cmd { } func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + var cmd tea.Cmd switch msg := msg.(type) { - case pubsub.Event[permission.PermissionRequest]: - return a, dialog.NewPermissionDialogCmd(msg.Payload) - case pubsub.Event[util.InfoMsg]: - a.status, _ = a.status.Update(msg) - case dialog.PermissionResponseMsg: - switch msg.Action { - case dialog.PermissionAllow: - a.app.Permissions.Grant(msg.Permission) - case dialog.PermissionAllowForSession: - a.app.Permissions.GrantPersistant(msg.Permission) - case dialog.PermissionDeny: - a.app.Permissions.Deny(msg.Permission) - } - case vimtea.EditorModeMsg: - a.editorMode = msg.Mode case tea.WindowSizeMsg: var cmds []tea.Cmd msg.Height -= 1 // Make space for the status bar @@ -109,6 +97,58 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.dialog = d.(core.DialogCmp) return a, tea.Batch(cmds...) + + // Status + case util.InfoMsg: + a.status, cmd = a.status.Update(msg) + return a, cmd + case pubsub.Event[logging.LogMessage]: + if msg.Payload.Persist { + switch msg.Payload.Level { + case "error": + a.status, cmd = a.status.Update(util.InfoMsg{ + Type: util.InfoTypeError, + Msg: msg.Payload.Message, + TTL: msg.Payload.PersistTime, + }) + case "info": + a.status, cmd = a.status.Update(util.InfoMsg{ + Type: util.InfoTypeInfo, + Msg: msg.Payload.Message, + TTL: msg.Payload.PersistTime, + }) + case "warn": + a.status, cmd = a.status.Update(util.InfoMsg{ + Type: util.InfoTypeWarn, + Msg: msg.Payload.Message, + TTL: msg.Payload.PersistTime, + }) + + default: + a.status, cmd = a.status.Update(util.InfoMsg{ + Type: util.InfoTypeInfo, + Msg: msg.Payload.Message, + TTL: msg.Payload.PersistTime, + }) + } + } + case util.ClearStatusMsg: + a.status, _ = a.status.Update(msg) + + // Permission + case pubsub.Event[permission.PermissionRequest]: + return a, dialog.NewPermissionDialogCmd(msg.Payload) + case dialog.PermissionResponseMsg: + switch msg.Action { + case dialog.PermissionAllow: + a.app.Permissions.Grant(msg.Permission) + case dialog.PermissionAllowForSession: + a.app.Permissions.GrantPersistant(msg.Permission) + case dialog.PermissionDeny: + a.app.Permissions.Deny(msg.Permission) + } + + // Dialog case core.DialogMsg: d, cmd := a.dialog.Update(msg) a.dialog = d.(core.DialogCmp) @@ -119,10 +159,13 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.dialog = d.(core.DialogCmp) a.dialogVisible = false return a, cmd + + // Editor + case vimtea.EditorModeMsg: + a.editorMode = msg.Mode + case page.PageChangeMsg: return a, a.moveToPage(msg.ID) - case util.InfoMsg: - a.status, _ = a.status.Update(msg) case tea.KeyMsg: if a.editorMode == vimtea.ModeNormal { switch { @@ -162,9 +205,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - var cmds []tea.Cmd - s, cmd := a.status.Update(msg) - a.status = s + a.status, cmd = a.status.Update(msg) cmds = append(cmds, cmd) if a.dialogVisible { d, cmd := a.dialog.Update(msg) @@ -172,8 +213,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, cmd) return a, tea.Batch(cmds...) } - p, cmd := a.pages[a.currentPage].Update(msg) - a.pages[a.currentPage] = p + a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg) cmds = append(cmds, cmd) return a, tea.Batch(cmds...) } diff --git a/internal/tui/util/util.go b/internal/tui/util/util.go index e006cbb43..2707009b3 100644 --- a/internal/tui/util/util.go +++ b/internal/tui/util/util.go @@ -1,6 +1,10 @@ package util -import tea "github.com/charmbracelet/bubbletea" +import ( + "time" + + tea "github.com/charmbracelet/bubbletea" +) func CmdHandler(msg tea.Msg) tea.Cmd { return func() tea.Msg { @@ -41,6 +45,7 @@ type ( InfoMsg struct { Type InfoType Msg string + TTL time.Duration } ClearStatusMsg struct{} ) |
