summaryrefslogtreecommitdiffhomepage
path: root/internal/tui
diff options
context:
space:
mode:
authorKujtim Hoxha <[email protected]>2025-04-09 19:07:39 +0200
committerKujtim Hoxha <[email protected]>2025-04-09 19:07:39 +0200
commitd39d52d95d6aaab67fb3a17efb9ed62cc290e72f (patch)
tree940bad6f0c847ceb213e5fc684b6d87cbf9d6996 /internal/tui
parent0d8d324ac6e640b95f4f2f62fd189399a959319a (diff)
downloadopencode-d39d52d95d6aaab67fb3a17efb9ed62cc290e72f.tar.gz
opencode-d39d52d95d6aaab67fb3a17efb9ed62cc290e72f.zip
finish logs page
Diffstat (limited to 'internal/tui')
-rw-r--r--internal/tui/components/core/status.go24
-rw-r--r--internal/tui/components/logs/details.go176
-rw-r--r--internal/tui/components/logs/table.go38
-rw-r--r--internal/tui/layout/bento.go55
-rw-r--r--internal/tui/page/logs.go19
-rw-r--r--internal/tui/tui.go84
-rw-r--r--internal/tui/util/util.go7
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{}
)