Detailed changes
@@ -107,16 +107,6 @@ func setupSubscriptions(app *app.App) (chan tea.Msg, func()) {
wg.Done()
}()
}
- {
- sub := app.Status.Subscribe(ctx)
- wg.Add(1)
- go func() {
- for ev := range sub {
- ch <- ev
- }
- wg.Done()
- }()
- }
return ch, func() {
cancel()
wg.Wait()
@@ -11,9 +11,7 @@ import (
"github.com/kujtimiihoxha/termai/internal/lsp/watcher"
"github.com/kujtimiihoxha/termai/internal/message"
"github.com/kujtimiihoxha/termai/internal/permission"
- "github.com/kujtimiihoxha/termai/internal/pubsub"
"github.com/kujtimiihoxha/termai/internal/session"
- "github.com/kujtimiihoxha/termai/internal/tui/util"
)
type App struct {
@@ -27,16 +25,14 @@ type App struct {
Logger logging.Interface
- Status *pubsub.Broker[util.InfoMsg]
ceanups []func()
}
func New(ctx context.Context, conn *sql.DB) *App {
cfg := config.Get()
q := db.New(conn)
- log := logging.NewLogger(logging.Options{
- Level: cfg.Log.Level,
- })
+ log := logging.Get()
+ log.SetLevel(cfg.Log.Level)
sessions := session.NewService(ctx, q)
messages := message.NewService(ctx, q)
@@ -46,7 +42,6 @@ func New(ctx context.Context, conn *sql.DB) *App {
Messages: messages,
Permissions: permission.NewPermissionService(),
Logger: log,
- Status: pubsub.NewBroker[util.InfoMsg](),
LSPClients: make(map[string]*lsp.Client),
}
@@ -65,6 +65,8 @@ type Config struct {
LSP map[string]LSPConfig `json:"lsp,omitempty"`
Model *Model `json:"model,omitempty"`
+
+ Debug bool `json:"debug,omitempty"`
}
var cfg *Config
@@ -90,8 +92,10 @@ func Load(debug bool) error {
// Add defaults
viper.SetDefault("data.directory", defaultDataDirectory)
if debug {
+ viper.SetDefault("debug", true)
viper.Set("log.level", "debug")
} else {
+ viper.SetDefault("debug", false)
viper.SetDefault("log.level", defaultLogLevel)
}
@@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
- "log"
"strings"
"sync"
@@ -15,8 +14,6 @@ import (
"github.com/kujtimiihoxha/termai/internal/llm/provider"
"github.com/kujtimiihoxha/termai/internal/llm/tools"
"github.com/kujtimiihoxha/termai/internal/message"
- "github.com/kujtimiihoxha/termai/internal/pubsub"
- "github.com/kujtimiihoxha/termai/internal/tui/util"
)
type Agent interface {
@@ -94,24 +91,13 @@ func (c *agent) processEvent(
assistantMsg.AppendContent(event.Content)
return c.Messages.Update(*assistantMsg)
case provider.EventError:
- // TODO: remove when realease
- log.Println("error", event.Error)
- c.App.Status.Publish(pubsub.UpdatedEvent, util.InfoMsg{
- Type: util.InfoTypeError,
- Msg: event.Error.Error(),
- })
+ c.App.Logger.PersistError(event.Error.Error())
return event.Error
case provider.EventWarning:
- c.App.Status.Publish(pubsub.UpdatedEvent, util.InfoMsg{
- Type: util.InfoTypeWarn,
- Msg: event.Info,
- })
+ c.App.Logger.PersistWarn(event.Info)
return nil
case provider.EventInfo:
- c.App.Status.Publish(pubsub.UpdatedEvent, util.InfoMsg{
- Type: util.InfoTypeInfo,
- Msg: event.Info,
- })
+ c.App.Logger.PersistInfo(event.Info)
case provider.EventComplete:
assistantMsg.SetToolCalls(event.Response.ToolCalls)
assistantMsg.AddFinish(event.Response.FinishReason)
@@ -4,10 +4,10 @@ import (
"context"
"encoding/json"
"fmt"
- "log"
"github.com/kujtimiihoxha/termai/internal/config"
"github.com/kujtimiihoxha/termai/internal/llm/tools"
+ "github.com/kujtimiihoxha/termai/internal/logging"
"github.com/kujtimiihoxha/termai/internal/permission"
"github.com/kujtimiihoxha/termai/internal/version"
@@ -22,6 +22,8 @@ type mcpTool struct {
permissions permission.Service
}
+var logger = logging.Get()
+
type MCPClient interface {
Initialize(
ctx context.Context,
@@ -141,13 +143,13 @@ func getTools(ctx context.Context, name string, m config.MCPServer, permissions
_, err := c.Initialize(ctx, initRequest)
if err != nil {
- log.Printf("error initializing mcp client: %s", err)
+ logger.Error("error initializing mcp client", "error", err)
return stdioTools
}
toolsRequest := mcp.ListToolsRequest{}
tools, err := c.ListTools(ctx, toolsRequest)
if err != nil {
- log.Printf("error listing tools: %s", err)
+ logger.Error("error listing tools", "error", err)
return stdioTools
}
for _, t := range tools.Tools {
@@ -170,7 +172,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
m.Args...,
)
if err != nil {
- log.Printf("error creating mcp client: %s", err)
+ logger.Error("error creating mcp client", "error", err)
continue
}
@@ -181,7 +183,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
client.WithHeaders(m.Headers),
)
if err != nil {
- log.Printf("error creating mcp client: %s", err)
+ logger.Error("error creating mcp client", "error", err)
continue
}
mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
@@ -4,14 +4,12 @@ import (
"context"
"encoding/json"
"errors"
- "log"
"github.com/google/generative-ai-go/genai"
"github.com/google/uuid"
"github.com/kujtimiihoxha/termai/internal/llm/models"
"github.com/kujtimiihoxha/termai/internal/llm/tools"
"github.com/kujtimiihoxha/termai/internal/message"
- "google.golang.org/api/googleapi"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
)
@@ -242,10 +240,6 @@ func (p *geminiProvider) StreamResponse(ctx context.Context, messages []message.
break
}
if err != nil {
- var apiErr *googleapi.Error
- if errors.As(err, &apiErr) {
- log.Printf("%s", apiErr.Body)
- }
eventChan <- ProviderEvent{
Type: EventError,
Error: err,
@@ -12,6 +12,11 @@ import (
const DefaultLevel = "info"
+const (
+ persistKeyArg = "$persist"
+ PersistTimeArg = "$persist_time"
+)
+
var levels = map[string]slog.Level{
"debug": slog.LevelDebug,
DefaultLevel: slog.LevelInfo,
@@ -36,16 +41,16 @@ func ValidLevels() []string {
return keys
}
-func NewLogger(opts Options) *Logger {
+func NewLogger(opts Options) Interface {
logger := &Logger{}
- broker := pubsub.NewBroker[Message]()
+ broker := pubsub.NewBroker[LogMessage]()
writer := &writer{
- messages: []Message{},
+ messages: []LogMessage{},
Broker: broker,
}
handler := slog.NewTextHandler(
- io.MultiWriter(append(opts.AdditionalWriters, writer)...),
+ io.MultiWriter(writer),
&slog.HandlerOptions{
Level: slog.Level(levels[opts.Level]),
},
@@ -57,8 +62,7 @@ func NewLogger(opts Options) *Logger {
}
type Options struct {
- Level string
- AdditionalWriters []io.Writer
+ Level string
}
type Logger struct {
@@ -66,6 +70,43 @@ type Logger struct {
writer *writer
}
+func (l *Logger) SetLevel(level string) {
+ if _, ok := levels[level]; !ok {
+ level = DefaultLevel
+ }
+ handler := slog.NewTextHandler(
+ io.MultiWriter(l.writer),
+ &slog.HandlerOptions{
+ Level: levels[level],
+ },
+ )
+ l.logger = slog.New(handler)
+}
+
+// PersistDebug implements Interface.
+func (l *Logger) PersistDebug(msg string, args ...any) {
+ args = append(args, persistKeyArg, true)
+ l.Debug(msg, args...)
+}
+
+// PersistError implements Interface.
+func (l *Logger) PersistError(msg string, args ...any) {
+ args = append(args, persistKeyArg, true)
+ l.Error(msg, args...)
+}
+
+// PersistInfo implements Interface.
+func (l *Logger) PersistInfo(msg string, args ...any) {
+ args = append(args, persistKeyArg, true)
+ l.Info(msg, args...)
+}
+
+// PersistWarn implements Interface.
+func (l *Logger) PersistWarn(msg string, args ...any) {
+ args = append(args, persistKeyArg, true)
+ l.Warn(msg, args...)
+}
+
func (l *Logger) Debug(msg string, args ...any) {
l.logger.Debug(msg, args...)
}
@@ -82,19 +123,19 @@ func (l *Logger) Error(msg string, args ...any) {
l.logger.Error(msg, args...)
}
-func (l *Logger) List() []Message {
+func (l *Logger) List() []LogMessage {
return l.writer.messages
}
-func (l *Logger) Get(id string) (Message, error) {
+func (l *Logger) Get(id string) (LogMessage, error) {
for _, msg := range l.writer.messages {
if msg.ID == id {
return msg, nil
}
}
- return Message{}, io.EOF
+ return LogMessage{}, io.EOF
}
-func (l *Logger) Subscribe(ctx context.Context) <-chan pubsub.Event[Message] {
+func (l *Logger) Subscribe(ctx context.Context) <-chan pubsub.Event[LogMessage] {
return l.writer.Subscribe(ctx)
}
@@ -11,7 +11,13 @@ type Interface interface {
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, args ...any)
- Subscribe(ctx context.Context) <-chan pubsub.Event[Message]
+ Subscribe(ctx context.Context) <-chan pubsub.Event[LogMessage]
- List() []Message
+ PersistDebug(msg string, args ...any)
+ PersistInfo(msg string, args ...any)
+ PersistWarn(msg string, args ...any)
+ PersistError(msg string, args ...any)
+ List() []LogMessage
+
+ SetLevel(level string)
}
@@ -4,13 +4,15 @@ import (
"time"
)
-// Message is the event payload for a log message
-type Message struct {
- ID string
- Time time.Time
- Level string
- Message string `json:"msg"`
- Attributes []Attr
+// LogMessage is the event payload for a log message
+type LogMessage struct {
+ ID string
+ Time time.Time
+ Level string
+ Persist bool // used when we want to show the mesage in the status bar
+ PersistTime time.Duration // used when we want to show the mesage in the status bar
+ Message string `json:"msg"`
+ Attributes []Attr
}
type Attr struct {
@@ -10,15 +10,15 @@ import (
)
type writer struct {
- messages []Message
- *pubsub.Broker[Message]
+ messages []LogMessage
+ *pubsub.Broker[LogMessage]
}
func (w *writer) Write(p []byte) (int, error) {
d := logfmt.NewDecoder(bytes.NewReader(p))
for d.ScanRecord() {
- msg := Message{
- ID: time.Now().Format(time.RFC3339Nano),
+ msg := LogMessage{
+ ID: fmt.Sprintf("%d", time.Now().UnixNano()),
}
for d.ScanKeyval() {
switch string(d.Key()) {
@@ -33,10 +33,20 @@ func (w *writer) Write(p []byte) (int, error) {
case "msg":
msg.Message = string(d.Value())
default:
- msg.Attributes = append(msg.Attributes, Attr{
- Key: string(d.Key()),
- Value: string(d.Value()),
- })
+ if string(d.Key()) == persistKeyArg {
+ msg.Persist = true
+ } else if string(d.Key()) == PersistTimeArg {
+ parsed, err := time.ParseDuration(string(d.Value()))
+ if err != nil {
+ continue
+ }
+ msg.PersistTime = parsed
+ } else {
+ msg.Attributes = append(msg.Attributes, Attr{
+ Key: string(d.Key()),
+ Value: string(d.Value()),
+ })
+ }
}
}
w.messages = append(w.messages, msg)
@@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"io"
- "log"
"os"
"os/exec"
"strings"
@@ -14,9 +13,13 @@ import (
"sync/atomic"
"time"
+ "github.com/kujtimiihoxha/termai/internal/config"
+ "github.com/kujtimiihoxha/termai/internal/logging"
"github.com/kujtimiihoxha/termai/internal/lsp/protocol"
)
+var logger = logging.Get()
+
type Client struct {
Cmd *exec.Cmd
stdin io.WriteCloser
@@ -357,6 +360,7 @@ func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
}
func (c *Client) CloseFile(ctx context.Context, filepath string) error {
+ cnf := config.Get()
uri := fmt.Sprintf("file://%s", filepath)
c.openFilesMu.Lock()
@@ -371,7 +375,10 @@ func (c *Client) CloseFile(ctx context.Context, filepath string) error {
URI: protocol.DocumentUri(uri),
},
}
- log.Println("Closing", params.TextDocument.URI.Dir())
+
+ if cnf.Debug {
+ logger.Debug("Closing file", "file", filepath)
+ }
if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
return err
}
@@ -393,6 +400,7 @@ func (c *Client) IsFileOpen(filepath string) bool {
// CloseAllFiles closes all currently open files
func (c *Client) CloseAllFiles(ctx context.Context) {
+ cnf := config.Get()
c.openFilesMu.Lock()
filesToClose := make([]string, 0, len(c.openFiles))
@@ -407,13 +415,13 @@ func (c *Client) CloseAllFiles(ctx context.Context) {
// Then close them all
for _, filePath := range filesToClose {
err := c.CloseFile(ctx, filePath)
- if err != nil && debug {
- log.Printf("Error closing file %s: %v", filePath, err)
+ if err != nil && cnf.Debug {
+ logger.Warn("Error closing file", "file", filePath, "error", err)
}
}
- if debug {
- log.Printf("Closed %d files", len(filesToClose))
+ if cnf.Debug {
+ logger.Debug("Closed all files", "files", filesToClose)
}
}
@@ -2,8 +2,8 @@ package lsp
import (
"encoding/json"
- "log"
+ "github.com/kujtimiihoxha/termai/internal/config"
"github.com/kujtimiihoxha/termai/internal/lsp/protocol"
"github.com/kujtimiihoxha/termai/internal/lsp/util"
)
@@ -17,7 +17,7 @@ func HandleWorkspaceConfiguration(params json.RawMessage) (any, error) {
func HandleRegisterCapability(params json.RawMessage) (any, error) {
var registerParams protocol.RegistrationParams
if err := json.Unmarshal(params, ®isterParams); err != nil {
- log.Printf("Error unmarshaling registration params: %v", err)
+ logger.Error("Error unmarshaling registration params", "error", err)
return nil, err
}
@@ -27,13 +27,13 @@ func HandleRegisterCapability(params json.RawMessage) (any, error) {
// Parse the registration options
optionsJSON, err := json.Marshal(reg.RegisterOptions)
if err != nil {
- log.Printf("Error marshaling registration options: %v", err)
+ logger.Error("Error marshaling registration options", "error", err)
continue
}
var options protocol.DidChangeWatchedFilesRegistrationOptions
if err := json.Unmarshal(optionsJSON, &options); err != nil {
- log.Printf("Error unmarshaling registration options: %v", err)
+ logger.Error("Error unmarshaling registration options", "error", err)
continue
}
@@ -53,7 +53,7 @@ func HandleApplyEdit(params json.RawMessage) (any, error) {
err := util.ApplyWorkspaceEdit(edit.Edit)
if err != nil {
- log.Printf("Error applying workspace edit: %v", err)
+ logger.Error("Error applying workspace edit", "error", err)
return protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: err.Error()}, nil
}
@@ -81,19 +81,22 @@ func notifyFileWatchRegistration(id string, watchers []protocol.FileSystemWatche
// Notifications
func HandleServerMessage(params json.RawMessage) {
+ cnf := config.Get()
var msg struct {
Type int `json:"type"`
Message string `json:"message"`
}
if err := json.Unmarshal(params, &msg); err == nil {
- log.Printf("Server message: %s\n", msg.Message)
+ if cnf.Debug {
+ logger.Debug("Server message", "type", msg.Type, "message", msg.Message)
+ }
}
}
func HandleDiagnostics(client *Client, params json.RawMessage) {
var diagParams protocol.PublishDiagnosticsParams
if err := json.Unmarshal(params, &diagParams); err != nil {
- log.Printf("Error unmarshaling diagnostic params: %v", err)
+ logger.Error("Error unmarshaling diagnostics params", "error", err)
return
}
@@ -6,12 +6,10 @@ import (
"encoding/json"
"fmt"
"io"
- "log"
- "os"
"strings"
-)
-var debug = os.Getenv("DEBUG") != ""
+ "github.com/kujtimiihoxha/termai/internal/config"
+)
// Write writes an LSP message to the given writer
func WriteMessage(w io.Writer, msg *Message) error {
@@ -19,10 +17,10 @@ func WriteMessage(w io.Writer, msg *Message) error {
if err != nil {
return fmt.Errorf("failed to marshal message: %w", err)
}
+ cnf := config.Get()
- if debug {
- log.Printf("%v", msg.Method)
- log.Printf("-> Sending: %s", string(data))
+ if cnf.Debug {
+ logger.Debug("Sending message to server", "method", msg.Method, "id", msg.ID)
}
_, err = fmt.Fprintf(w, "Content-Length: %d\r\n\r\n", len(data))
@@ -40,6 +38,7 @@ func WriteMessage(w io.Writer, msg *Message) error {
// ReadMessage reads a single LSP message from the given reader
func ReadMessage(r *bufio.Reader) (*Message, error) {
+ cnf := config.Get()
// Read headers
var contentLength int
for {
@@ -49,8 +48,8 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
}
line = strings.TrimSpace(line)
- if debug {
- log.Printf("<- Header: %s", line)
+ if cnf.Debug {
+ logger.Debug("Received header", "line", line)
}
if line == "" {
@@ -65,8 +64,8 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
}
}
- if debug {
- log.Printf("<- Reading content with length: %d", contentLength)
+ if cnf.Debug {
+ logger.Debug("Content-Length", "length", contentLength)
}
// Read content
@@ -76,8 +75,8 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
return nil, fmt.Errorf("failed to read content: %w", err)
}
- if debug {
- log.Printf("<- Received: %s", string(content))
+ if cnf.Debug {
+ logger.Debug("Received content", "content", string(content))
}
// Parse message
@@ -91,19 +90,20 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
// handleMessages reads and dispatches messages in a loop
func (c *Client) handleMessages() {
+ cnf := config.Get()
for {
msg, err := ReadMessage(c.stdout)
if err != nil {
- if debug {
- log.Printf("Error reading message: %v", err)
+ if cnf.Debug {
+ logger.Error("Error reading message", "error", err)
}
return
}
// Handle server->client request (has both Method and ID)
if msg.Method != "" && msg.ID != 0 {
- if debug {
- log.Printf("Received request from server: method=%s id=%d", msg.Method, msg.ID)
+ if cnf.Debug {
+ logger.Debug("Received request from server", "method", msg.Method, "id", msg.ID)
}
response := &Message{
@@ -143,7 +143,7 @@ func (c *Client) handleMessages() {
// Send response back to server
if err := WriteMessage(c.stdin, response); err != nil {
- log.Printf("Error sending response to server: %v", err)
+ logger.Error("Error sending response to server", "error", err)
}
continue
@@ -156,12 +156,12 @@ func (c *Client) handleMessages() {
c.notificationMu.RUnlock()
if ok {
- if debug {
- log.Printf("Handling notification: %s", msg.Method)
+ if cnf.Debug {
+ logger.Debug("Handling notification", "method", msg.Method)
}
go handler(msg.Params)
- } else if debug {
- log.Printf("No handler for notification: %s", msg.Method)
+ } else if cnf.Debug {
+ logger.Debug("No handler for notification", "method", msg.Method)
}
continue
}
@@ -173,13 +173,13 @@ func (c *Client) handleMessages() {
c.handlersMu.RUnlock()
if ok {
- if debug {
- log.Printf("Sending response for ID %d to handler", msg.ID)
+ if cnf.Debug {
+ logger.Debug("Received response for request", "id", msg.ID)
}
ch <- msg
close(ch)
- } else if debug {
- log.Printf("No handler for response ID: %d", msg.ID)
+ } else if cnf.Debug {
+ logger.Debug("No handler for response", "id", msg.ID)
}
}
}
@@ -187,10 +187,11 @@ func (c *Client) handleMessages() {
// Call makes a request and waits for the response
func (c *Client) Call(ctx context.Context, method string, params any, result any) error {
+ cnf := config.Get()
id := c.nextID.Add(1)
- if debug {
- log.Printf("Making call: method=%s id=%d", method, id)
+ if cnf.Debug {
+ logger.Debug("Making call", "method", method, "id", id)
}
msg, err := NewRequest(id, method, params)
@@ -215,15 +216,15 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
return fmt.Errorf("failed to send request: %w", err)
}
- if debug {
- log.Printf("Waiting for response to request ID: %d", id)
+ if cnf.Debug {
+ logger.Debug("Request sent", "method", method, "id", id)
}
// Wait for response
resp := <-ch
- if debug {
- log.Printf("Received response for request ID: %d", id)
+ if cnf.Debug {
+ logger.Debug("Received response", "id", id)
}
if resp.Error != nil {
@@ -247,8 +248,9 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
// Notify sends a notification (a request without an ID that doesn't expect a response)
func (c *Client) Notify(ctx context.Context, method string, params any) error {
- if debug {
- log.Printf("Sending notification: method=%s", method)
+ cnf := config.Get()
+ if cnf.Debug {
+ logger.Debug("Sending notification", "method", method)
}
msg, err := NewNotification(method, params)
@@ -3,7 +3,6 @@ package watcher
import (
"context"
"fmt"
- "log"
"os"
"path/filepath"
"strings"
@@ -11,11 +10,13 @@ import (
"time"
"github.com/fsnotify/fsnotify"
+ "github.com/kujtimiihoxha/termai/internal/config"
+ "github.com/kujtimiihoxha/termai/internal/logging"
"github.com/kujtimiihoxha/termai/internal/lsp"
"github.com/kujtimiihoxha/termai/internal/lsp/protocol"
)
-var debug = false // Force debug logging on
+var logger = logging.Get()
// WorkspaceWatcher manages LSP file watching
type WorkspaceWatcher struct {
@@ -43,6 +44,7 @@ func NewWorkspaceWatcher(client *lsp.Client) *WorkspaceWatcher {
// AddRegistrations adds file watchers to track
func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watchers []protocol.FileSystemWatcher) {
+ cnf := config.Get()
w.registrationMu.Lock()
defer w.registrationMu.Unlock()
@@ -50,31 +52,35 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
w.registrations = append(w.registrations, watchers...)
// Print detailed registration information for debugging
- if debug {
- log.Printf("Added %d file watcher registrations (id: %s), total: %d",
- len(watchers), id, len(w.registrations))
+ if cnf.Debug {
+ logger.Debug("Adding file watcher registrations",
+ "id", id,
+ "watchers", len(watchers),
+ "total", len(w.registrations),
+ "watchers", watchers,
+ )
for i, watcher := range watchers {
- log.Printf("Registration #%d raw data:", i+1)
+ logger.Debug("Registration", "index", i+1)
// Log the GlobPattern
switch v := watcher.GlobPattern.Value.(type) {
case string:
- log.Printf(" GlobPattern: string pattern '%s'", v)
+ logger.Debug("GlobPattern", "pattern", v)
case protocol.RelativePattern:
- log.Printf(" GlobPattern: RelativePattern with pattern '%s'", v.Pattern)
+ logger.Debug("GlobPattern", "pattern", v.Pattern)
// Log BaseURI details
switch u := v.BaseURI.Value.(type) {
case string:
- log.Printf(" BaseURI: string '%s'", u)
+ logger.Debug("BaseURI", "baseURI", u)
case protocol.DocumentUri:
- log.Printf(" BaseURI: DocumentUri '%s'", u)
+ logger.Debug("BaseURI", "baseURI", u)
default:
- log.Printf(" BaseURI: unknown type %T", u)
+ logger.Debug("BaseURI", "baseURI", u)
}
default:
- log.Printf(" GlobPattern: unknown type %T", v)
+ logger.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v))
}
// Log WatchKind
@@ -82,11 +88,8 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
if watcher.Kind != nil {
watchKind = *watcher.Kind
}
- log.Printf(" WatchKind: %d (Create:%v, Change:%v, Delete:%v)",
- watchKind,
- watchKind&protocol.WatchCreate != 0,
- watchKind&protocol.WatchChange != 0,
- watchKind&protocol.WatchDelete != 0)
+
+ logger.Debug("WatchKind", "kind", watchKind)
// Test match against some example paths
testPaths := []string{
@@ -96,7 +99,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
for _, testPath := range testPaths {
isMatch := w.matchesPattern(testPath, watcher.GlobPattern)
- log.Printf(" Test path '%s': %v", testPath, isMatch)
+ logger.Debug("Test path", "path", testPath, "matches", isMatch)
}
}
}
@@ -114,10 +117,9 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
// Skip directories that should be excluded
if d.IsDir() {
- log.Println(path)
if path != w.workspacePath && shouldExcludeDir(path) {
- if debug {
- log.Printf("Skipping excluded directory!!: %s", path)
+ if cnf.Debug {
+ logger.Debug("Skipping excluded directory", "path", path)
}
return filepath.SkipDir
}
@@ -136,18 +138,23 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
})
elapsedTime := time.Since(startTime)
- if debug {
- log.Printf("Workspace scan complete: processed %d files in %.2f seconds", filesOpened, elapsedTime.Seconds())
+ if cnf.Debug {
+ logger.Debug("Workspace scan complete",
+ "filesOpened", filesOpened,
+ "elapsedTime", elapsedTime.Seconds(),
+ "workspacePath", w.workspacePath,
+ )
}
- if err != nil && debug {
- log.Printf("Error scanning workspace for files to open: %v", err)
+ if err != nil && cnf.Debug {
+ logger.Debug("Error scanning workspace for files to open", "error", err)
}
}()
}
// WatchWorkspace sets up file watching for a workspace
func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath string) {
+ cnf := config.Get()
w.workspacePath = workspacePath
// Register handler for file watcher registrations from the server
@@ -157,7 +164,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
watcher, err := fsnotify.NewWatcher()
if err != nil {
- log.Fatalf("Error creating watcher: %v", err)
+ logger.Error("Error creating watcher", "error", err)
}
defer watcher.Close()
@@ -170,8 +177,8 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
// Skip excluded directories (except workspace root)
if d.IsDir() && path != workspacePath {
if shouldExcludeDir(path) {
- if debug {
- log.Printf("Skipping watching excluded directory: %s", path)
+ if cnf.Debug {
+ logger.Debug("Skipping excluded directory", "path", path)
}
return filepath.SkipDir
}
@@ -181,14 +188,14 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
if d.IsDir() {
err = watcher.Add(path)
if err != nil {
- log.Printf("Error watching path %s: %v", path, err)
+ logger.Error("Error watching path", "path", path, "error", err)
}
}
return nil
})
if err != nil {
- log.Fatalf("Error walking workspace: %v", err)
+ logger.Error("Error walking workspace", "error", err)
}
// Event loop
@@ -210,7 +217,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
// Skip excluded directories
if !shouldExcludeDir(event.Name) {
if err := watcher.Add(event.Name); err != nil {
- log.Printf("Error watching new directory: %v", err)
+ logger.Error("Error adding directory to watcher", "path", event.Name, "error", err)
}
}
} else {
@@ -223,10 +230,15 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
}
// Debug logging
- if debug {
+ if cnf.Debug {
matched, kind := w.isPathWatched(event.Name)
- log.Printf("Event: %s, Op: %s, Watched: %v, Kind: %d",
- event.Name, event.Op.String(), matched, kind)
+ logger.Debug("File event",
+ "path", event.Name,
+ "operation", event.Op.String(),
+ "watched", matched,
+ "kind", kind,
+ )
+
}
// Check if this path should be watched according to server registrations
@@ -265,7 +277,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
if !ok {
return
}
- log.Printf("Watcher error: %v\n", err)
+ logger.Error("Error watching file", "error", err)
}
}
}
@@ -390,7 +402,7 @@ func matchesSimpleGlob(pattern, path string) bool {
// Fall back to simple matching for simpler patterns
matched, err := filepath.Match(pattern, path)
if err != nil {
- log.Printf("Error matching pattern %s: %v", pattern, err)
+ logger.Error("Error matching pattern", "pattern", pattern, "path", path, "error", err)
return false
}
@@ -401,7 +413,7 @@ func matchesSimpleGlob(pattern, path string) bool {
func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPattern) bool {
patternInfo, err := pattern.AsPattern()
if err != nil {
- log.Printf("Error parsing pattern: %v", err)
+ logger.Error("Error parsing pattern", "pattern", pattern, "error", err)
return false
}
@@ -426,7 +438,7 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt
// Make path relative to basePath for matching
relPath, err := filepath.Rel(basePath, path)
if err != nil {
- log.Printf("Error getting relative path for %s: %v", path, err)
+ logger.Error("Error getting relative path", "path", path, "basePath", basePath, "error", err)
return false
}
relPath = filepath.ToSlash(relPath)
@@ -467,21 +479,25 @@ func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, chan
if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
err := w.client.NotifyChange(ctx, filePath)
if err != nil {
- log.Printf("Error notifying change: %v", err)
+ logger.Error("Error notifying change", "error", err)
}
return
}
// Notify LSP server about the file event using didChangeWatchedFiles
if err := w.notifyFileEvent(ctx, uri, changeType); err != nil {
- log.Printf("Error notifying LSP server about file event: %v", err)
+ logger.Error("Error notifying LSP server about file event", "error", err)
}
}
// notifyFileEvent sends a didChangeWatchedFiles notification for a file event
func (w *WorkspaceWatcher) notifyFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) error {
- if debug {
- log.Printf("Notifying file event: %s (type: %d)", uri, changeType)
+ cnf := config.Get()
+ if cnf.Debug {
+ logger.Debug("Notifying file event",
+ "uri", uri,
+ "changeType", changeType,
+ )
}
params := protocol.DidChangeWatchedFilesParams{
@@ -575,7 +591,7 @@ func shouldExcludeDir(dirPath string) bool {
// shouldExcludeFile returns true if the file should be excluded from opening
func shouldExcludeFile(filePath string) bool {
fileName := filepath.Base(filePath)
-
+ cnf := config.Get()
// Skip dot files
if strings.HasPrefix(fileName, ".") {
return true
@@ -601,8 +617,15 @@ func shouldExcludeFile(filePath string) bool {
// Skip large files
if info.Size() > maxFileSize {
- if debug {
- log.Printf("Skipping large file: %s (%.2f MB)", filePath, float64(info.Size())/(1024*1024))
+ if cnf.Debug {
+ logger.Debug("Skipping large file",
+ "path", filePath,
+ "size", info.Size(),
+ "maxSize", maxFileSize,
+ "debug", cnf.Debug,
+ "sizeMB", float64(info.Size())/(1024*1024),
+ "maxSizeMB", float64(maxFileSize)/(1024*1024),
+ )
}
return true
}
@@ -612,6 +635,7 @@ func shouldExcludeFile(filePath string) bool {
// openMatchingFile opens a file if it matches any of the registered patterns
func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
+ cnf := config.Get()
// Skip directories
info, err := os.Stat(path)
if err != nil || info.IsDir() {
@@ -626,8 +650,8 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
// Check if this path should be watched according to server registrations
if watched, _ := w.isPathWatched(path); watched {
// Don't need to check if it's already open - the client.OpenFile handles that
- if err := w.client.OpenFile(ctx, path); err != nil && debug {
- log.Printf("Error opening file %s: %v", path, err)
+ if err := w.client.OpenFile(ctx, path); err != nil && cnf.Debug {
+ logger.Error("Error opening file", "path", path, "error", err)
}
}
}
@@ -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).
@@ -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
- 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},
@@ -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{}
)
@@ -1,21 +1,9 @@
package main
import (
- "log"
- "os"
-
"github.com/kujtimiihoxha/termai/cmd"
)
func main() {
- // Create a log file and make that the log output DEBUG
- // TODO: remove this on release
- logfile, err := os.OpenFile("debug.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
- if err != nil {
- panic(err)
- }
-
- log.SetOutput(logfile)
-
cmd.Execute()
}