mirror of
https://github.com/aljazceru/opencode.git
synced 2026-01-09 02:44:55 +01:00
finish logs page
This commit is contained in:
@@ -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).
|
||||
|
||||
176
internal/tui/components/logs/details.go
Normal file
176
internal/tui/components/logs/details.go
Normal file
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
t, cmd := i.table.Update(msg)
|
||||
i.table = t
|
||||
return i, cmd
|
||||
}
|
||||
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},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user