on main help sign in
finish logs page
d39d52dKujtim Hoxha
cmd/root.go−10
@@ -107,16 +107,6 @@ func setupSubscriptions(app *app.App) (chan tea.Msg, func()) {
107 wg.Done()
107 wg.Done()
108 }()
108 }()
109 }
109 }
110− {
111− sub := app.Status.Subscribe(ctx)
112− wg.Add(1)
113− go func() {
114− for ev := range sub {
115− ch <- ev
116− }
117− wg.Done()
118− }()
119− }
120 return ch, func() {
110 return ch, func() {
121 cancel()
111 cancel()
122 wg.Wait()
112 wg.Wait()
internal/app/services.go+2−7
@@ -11,9 +11,7 @@ import (
11 "github.com/kujtimiihoxha/termai/internal/lsp/watcher"
11 "github.com/kujtimiihoxha/termai/internal/lsp/watcher"
12 "github.com/kujtimiihoxha/termai/internal/message"
12 "github.com/kujtimiihoxha/termai/internal/message"
13 "github.com/kujtimiihoxha/termai/internal/permission"
13 "github.com/kujtimiihoxha/termai/internal/permission"
14− "github.com/kujtimiihoxha/termai/internal/pubsub"
15 "github.com/kujtimiihoxha/termai/internal/session"
14 "github.com/kujtimiihoxha/termai/internal/session"
16− "github.com/kujtimiihoxha/termai/internal/tui/util"
17)
15)
18
16
19type App struct {
17type App struct {
@@ -27,16 +25,14 @@ type App struct {
27
25
28 Logger logging.Interface
26 Logger logging.Interface
29
27
30− Status *pubsub.Broker[util.InfoMsg]
31 ceanups []func()
28 ceanups []func()
32}
29}
33
30
34func New(ctx context.Context, conn *sql.DB) *App {
31func New(ctx context.Context, conn *sql.DB) *App {
35 cfg := config.Get()
32 cfg := config.Get()
36 q := db.New(conn)
33 q := db.New(conn)
37− log := logging.NewLogger(logging.Options{
34+ log := logging.Get()
38− Level: cfg.Log.Level,
35+ log.SetLevel(cfg.Log.Level)
39− })
40 sessions := session.NewService(ctx, q)
36 sessions := session.NewService(ctx, q)
41 messages := message.NewService(ctx, q)
37 messages := message.NewService(ctx, q)
42
38
@@ -46,7 +42,6 @@ func New(ctx context.Context, conn *sql.DB) *App {
46 Messages: messages,
42 Messages: messages,
47 Permissions: permission.NewPermissionService(),
43 Permissions: permission.NewPermissionService(),
48 Logger: log,
44 Logger: log,
49− Status: pubsub.NewBroker[util.InfoMsg](),
50 LSPClients: make(map[string]*lsp.Client),
45 LSPClients: make(map[string]*lsp.Client),
51 }
46 }
52
47
internal/config/config.go+4
@@ -65,6 +65,8 @@ type Config struct {
65 LSP map[string]LSPConfig `json:"lsp,omitempty"`
65 LSP map[string]LSPConfig `json:"lsp,omitempty"`
66
66
67 Model *Model `json:"model,omitempty"`
67 Model *Model `json:"model,omitempty"`
68+
69+ Debug bool `json:"debug,omitempty"`
68}
70}
69
71
70var cfg *Config
72var cfg *Config
@@ -90,8 +92,10 @@ func Load(debug bool) error {
90 // Add defaults
92 // Add defaults
91 viper.SetDefault("data.directory", defaultDataDirectory)
93 viper.SetDefault("data.directory", defaultDataDirectory)
92 if debug {
94 if debug {
95+ viper.SetDefault("debug", true)
93 viper.Set("log.level", "debug")
96 viper.Set("log.level", "debug")
94 } else {
97 } else {
98+ viper.SetDefault("debug", false)
95 viper.SetDefault("log.level", defaultLogLevel)
99 viper.SetDefault("log.level", defaultLogLevel)
96 }
100 }
97
101
internal/llm/agent/agent.go+3−17
@@ -4,7 +4,6 @@ import (
4 "context"
4 "context"
5 "errors"
5 "errors"
6 "fmt"
6 "fmt"
7− "log"
8 "strings"
7 "strings"
9 "sync"
8 "sync"
10
9
@@ -15,8 +14,6 @@ import (
15 "github.com/kujtimiihoxha/termai/internal/llm/provider"
14 "github.com/kujtimiihoxha/termai/internal/llm/provider"
16 "github.com/kujtimiihoxha/termai/internal/llm/tools"
15 "github.com/kujtimiihoxha/termai/internal/llm/tools"
17 "github.com/kujtimiihoxha/termai/internal/message"
16 "github.com/kujtimiihoxha/termai/internal/message"
18− "github.com/kujtimiihoxha/termai/internal/pubsub"
19− "github.com/kujtimiihoxha/termai/internal/tui/util"
20)
17)
21
18
22type Agent interface {
19type Agent interface {
@@ -94,24 +91,13 @@ func (c *agent) processEvent(
94 assistantMsg.AppendContent(event.Content)
91 assistantMsg.AppendContent(event.Content)
95 return c.Messages.Update(*assistantMsg)
92 return c.Messages.Update(*assistantMsg)
96 case provider.EventError:
93 case provider.EventError:
97− // TODO: remove when realease
98− log.Println("error", event.Error)
99− c.App.Status.Publish(pubsub.UpdatedEvent, util.InfoMsg{
100− Type: util.InfoTypeError,
101− Msg: event.Error.Error(),
94+ c.App.Logger.PersistError(event.Error.Error())
102− })
103 return event.Error
95 return event.Error
104 case provider.EventWarning:
96 case provider.EventWarning:
105− c.App.Status.Publish(pubsub.UpdatedEvent, util.InfoMsg{
106− Type: util.InfoTypeWarn,
107− Msg: event.Info,
97+ c.App.Logger.PersistWarn(event.Info)
108− })
109 return nil
98 return nil
110 case provider.EventInfo:
99 case provider.EventInfo:
111− c.App.Status.Publish(pubsub.UpdatedEvent, util.InfoMsg{
112− Type: util.InfoTypeInfo,
113− Msg: event.Info,
100+ c.App.Logger.PersistInfo(event.Info)
114− })
115 case provider.EventComplete:
101 case provider.EventComplete:
116 assistantMsg.SetToolCalls(event.Response.ToolCalls)
102 assistantMsg.SetToolCalls(event.Response.ToolCalls)
117 assistantMsg.AddFinish(event.Response.FinishReason)
103 assistantMsg.AddFinish(event.Response.FinishReason)
internal/llm/agent/mcp-tools.go+7−5
@@ -4,10 +4,10 @@ import (
4 "context"
4 "context"
5 "encoding/json"
5 "encoding/json"
6 "fmt"
6 "fmt"
7− "log"
8
7
9 "github.com/kujtimiihoxha/termai/internal/config"
8 "github.com/kujtimiihoxha/termai/internal/config"
10 "github.com/kujtimiihoxha/termai/internal/llm/tools"
9 "github.com/kujtimiihoxha/termai/internal/llm/tools"
10+ "github.com/kujtimiihoxha/termai/internal/logging"
11 "github.com/kujtimiihoxha/termai/internal/permission"
11 "github.com/kujtimiihoxha/termai/internal/permission"
12 "github.com/kujtimiihoxha/termai/internal/version"
12 "github.com/kujtimiihoxha/termai/internal/version"
13
13
@@ -22,6 +22,8 @@ type mcpTool struct {
22 permissions permission.Service
22 permissions permission.Service
23}
23}
24
24
25+var logger = logging.Get()
26+
25type MCPClient interface {
27type MCPClient interface {
26 Initialize(
28 Initialize(
27 ctx context.Context,
29 ctx context.Context,
@@ -141,13 +143,13 @@ func getTools(ctx context.Context, name string, m config.MCPServer, permissions
141
143
142 _, err := c.Initialize(ctx, initRequest)
144 _, err := c.Initialize(ctx, initRequest)
143 if err != nil {
145 if err != nil {
144− log.Printf("error initializing mcp client: %s", err)
146+ logger.Error("error initializing mcp client", "error", err)
145 return stdioTools
147 return stdioTools
146 }
148 }
147 toolsRequest := mcp.ListToolsRequest{}
149 toolsRequest := mcp.ListToolsRequest{}
148 tools, err := c.ListTools(ctx, toolsRequest)
150 tools, err := c.ListTools(ctx, toolsRequest)
149 if err != nil {
151 if err != nil {
150− log.Printf("error listing tools: %s", err)
152+ logger.Error("error listing tools", "error", err)
151 return stdioTools
153 return stdioTools
152 }
154 }
153 for _, t := range tools.Tools {
155 for _, t := range tools.Tools {
@@ -170,7 +172,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
170 m.Args...,
172 m.Args...,
171 )
173 )
172 if err != nil {
174 if err != nil {
173− log.Printf("error creating mcp client: %s", err)
175+ logger.Error("error creating mcp client", "error", err)
174 continue
176 continue
175 }
177 }
176
178
@@ -181,7 +183,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
181 client.WithHeaders(m.Headers),
183 client.WithHeaders(m.Headers),
182 )
184 )
183 if err != nil {
185 if err != nil {
184− log.Printf("error creating mcp client: %s", err)
186+ logger.Error("error creating mcp client", "error", err)
185 continue
187 continue
186 }
188 }
187 mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
189 mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
internal/llm/provider/gemini.go−6
@@ -4,14 +4,12 @@ import (
4 "context"
4 "context"
5 "encoding/json"
5 "encoding/json"
6 "errors"
6 "errors"
7− "log"
8
7
9 "github.com/google/generative-ai-go/genai"
8 "github.com/google/generative-ai-go/genai"
10 "github.com/google/uuid"
9 "github.com/google/uuid"
11 "github.com/kujtimiihoxha/termai/internal/llm/models"
10 "github.com/kujtimiihoxha/termai/internal/llm/models"
12 "github.com/kujtimiihoxha/termai/internal/llm/tools"
11 "github.com/kujtimiihoxha/termai/internal/llm/tools"
13 "github.com/kujtimiihoxha/termai/internal/message"
12 "github.com/kujtimiihoxha/termai/internal/message"
14− "google.golang.org/api/googleapi"
15 "google.golang.org/api/iterator"
13 "google.golang.org/api/iterator"
16 "google.golang.org/api/option"
14 "google.golang.org/api/option"
17)
15)
@@ -242,10 +240,6 @@ func (p *geminiProvider) StreamResponse(ctx context.Context, messages []message.
242 break
240 break
243 }
241 }
244 if err != nil {
242 if err != nil {
245− var apiErr *googleapi.Error
246− if errors.As(err, &apiErr) {
247− log.Printf("%s", apiErr.Body)
248− }
249 eventChan <- ProviderEvent{
243 eventChan <- ProviderEvent{
250 Type: EventError,
244 Type: EventError,
251 Error: err,
245 Error: err,
internal/logging/logger.go+51−10
@@ -12,6 +12,11 @@ import (
12
12
13const DefaultLevel = "info"
13const DefaultLevel = "info"
14
14
15+const (
16+ persistKeyArg = "$persist"
17+ PersistTimeArg = "$persist_time"
18+)
19+
15var levels = map[string]slog.Level{
20var levels = map[string]slog.Level{
16 "debug": slog.LevelDebug,
21 "debug": slog.LevelDebug,
17 DefaultLevel: slog.LevelInfo,
22 DefaultLevel: slog.LevelInfo,
@@ -36,16 +41,16 @@ func ValidLevels() []string {
36 return keys
41 return keys
37}
42}
38
43
39−func NewLogger(opts Options) *Logger {
44+func NewLogger(opts Options) Interface {
40 logger := &Logger{}
45 logger := &Logger{}
41− broker := pubsub.NewBroker[Message]()
46+ broker := pubsub.NewBroker[LogMessage]()
42 writer := &writer{
47 writer := &writer{
43− messages: []Message{},
48+ messages: []LogMessage{},
44 Broker: broker,
49 Broker: broker,
45 }
50 }
46
51
47 handler := slog.NewTextHandler(
52 handler := slog.NewTextHandler(
48− io.MultiWriter(append(opts.AdditionalWriters, writer)...),
53+ io.MultiWriter(writer),
49 &slog.HandlerOptions{
54 &slog.HandlerOptions{
50 Level: slog.Level(levels[opts.Level]),
55 Level: slog.Level(levels[opts.Level]),
51 },
56 },
@@ -57,8 +62,7 @@ func NewLogger(opts Options) *Logger {
57}
62}
58
63
59type Options struct {
64type Options struct {
60− Level string
65+ Level string
61− AdditionalWriters []io.Writer
62}
66}
63
67
64type Logger struct {
68type Logger struct {
@@ -66,6 +70,43 @@ type Logger struct {
66 writer *writer
70 writer *writer
67}
71}
68
72
73+func (l *Logger) SetLevel(level string) {
74+ if _, ok := levels[level]; !ok {
75+ level = DefaultLevel
76+ }
77+ handler := slog.NewTextHandler(
78+ io.MultiWriter(l.writer),
79+ &slog.HandlerOptions{
80+ Level: levels[level],
81+ },
82+ )
83+ l.logger = slog.New(handler)
84+}
85+
86+// PersistDebug implements Interface.
87+func (l *Logger) PersistDebug(msg string, args ...any) {
88+ args = append(args, persistKeyArg, true)
89+ l.Debug(msg, args...)
90+}
91+
92+// PersistError implements Interface.
93+func (l *Logger) PersistError(msg string, args ...any) {
94+ args = append(args, persistKeyArg, true)
95+ l.Error(msg, args...)
96+}
97+
98+// PersistInfo implements Interface.
99+func (l *Logger) PersistInfo(msg string, args ...any) {
100+ args = append(args, persistKeyArg, true)
101+ l.Info(msg, args...)
102+}
103+
104+// PersistWarn implements Interface.
105+func (l *Logger) PersistWarn(msg string, args ...any) {
106+ args = append(args, persistKeyArg, true)
107+ l.Warn(msg, args...)
108+}
109+
69func (l *Logger) Debug(msg string, args ...any) {
110func (l *Logger) Debug(msg string, args ...any) {
70 l.logger.Debug(msg, args...)
111 l.logger.Debug(msg, args...)
71}
112}
@@ -82,19 +123,19 @@ func (l *Logger) Error(msg string, args ...any) {
82 l.logger.Error(msg, args...)
123 l.logger.Error(msg, args...)
83}
124}
84
125
85−func (l *Logger) List() []Message {
126+func (l *Logger) List() []LogMessage {
86 return l.writer.messages
127 return l.writer.messages
87}
128}
88
129
89−func (l *Logger) Get(id string) (Message, error) {
130+func (l *Logger) Get(id string) (LogMessage, error) {
90 for _, msg := range l.writer.messages {
131 for _, msg := range l.writer.messages {
91 if msg.ID == id {
132 if msg.ID == id {
92 return msg, nil
133 return msg, nil
93 }
134 }
94 }
135 }
95− return Message{}, io.EOF
136+ return LogMessage{}, io.EOF
96}
137}
97
138
98−func (l *Logger) Subscribe(ctx context.Context) <-chan pubsub.Event[Message] {
139+func (l *Logger) Subscribe(ctx context.Context) <-chan pubsub.Event[LogMessage] {
99 return l.writer.Subscribe(ctx)
140 return l.writer.Subscribe(ctx)
100}
141}
internal/logging/logging.go+8−2
@@ -11,7 +11,13 @@ type Interface interface {
11 Info(msg string, args ...any)
11 Info(msg string, args ...any)
12 Warn(msg string, args ...any)
12 Warn(msg string, args ...any)
13 Error(msg string, args ...any)
13 Error(msg string, args ...any)
14− Subscribe(ctx context.Context) <-chan pubsub.Event[Message]
14+ Subscribe(ctx context.Context) <-chan pubsub.Event[LogMessage]
15
15
16− List() []Message
16+ PersistDebug(msg string, args ...any)
17+ PersistInfo(msg string, args ...any)
18+ PersistWarn(msg string, args ...any)
19+ PersistError(msg string, args ...any)
20+ List() []LogMessage
21+
22+ SetLevel(level string)
17}
23}
internal/logging/message.go+9−7
@@ -4,13 +4,15 @@ import (
4 "time"
4 "time"
5)
5)
6
6
7−// Message is the event payload for a log message
7+// LogMessage is the event payload for a log message
8−type Message struct {
8+type LogMessage struct {
9− ID string
9+ ID string
10− Time time.Time
10+ Time time.Time
11− Level string
11+ Level string
12+ Persist bool // used when we want to show the mesage in the status bar
13+ PersistTime time.Duration // used when we want to show the mesage in the status bar
12− Message string `json:"msg"`
14+ Message string `json:"msg"`
13− Attributes []Attr
15+ Attributes []Attr
14}
16}
15
17
16type Attr struct {
18type Attr struct {
internal/logging/writer.go+18−8
@@ -10,15 +10,15 @@ import (
10)
10)
11
11
12type writer struct {
12type writer struct {
13− messages []Message
13+ messages []LogMessage
14− *pubsub.Broker[Message]
14+ *pubsub.Broker[LogMessage]
15}
15}
16
16
17func (w *writer) Write(p []byte) (int, error) {
17func (w *writer) Write(p []byte) (int, error) {
18 d := logfmt.NewDecoder(bytes.NewReader(p))
18 d := logfmt.NewDecoder(bytes.NewReader(p))
19 for d.ScanRecord() {
19 for d.ScanRecord() {
20− msg := Message{
20+ msg := LogMessage{
21− ID: time.Now().Format(time.RFC3339Nano),
21+ ID: fmt.Sprintf("%d", time.Now().UnixNano()),
22 }
22 }
23 for d.ScanKeyval() {
23 for d.ScanKeyval() {
24 switch string(d.Key()) {
24 switch string(d.Key()) {
@@ -33,10 +33,20 @@ func (w *writer) Write(p []byte) (int, error) {
33 case "msg":
33 case "msg":
34 msg.Message = string(d.Value())
34 msg.Message = string(d.Value())
35 default:
35 default:
36+ if string(d.Key()) == persistKeyArg {
37+ msg.Persist = true
38+ } else if string(d.Key()) == PersistTimeArg {
39+ parsed, err := time.ParseDuration(string(d.Value()))
40+ if err != nil {
41+ continue
42+ }
43+ msg.PersistTime = parsed
44+ } else {
36− msg.Attributes = append(msg.Attributes, Attr{
45+ msg.Attributes = append(msg.Attributes, Attr{
37− Key: string(d.Key()),
46+ Key: string(d.Key()),
38− Value: string(d.Value()),
47+ Value: string(d.Value()),
39− })
48+ })
49+ }
40 }
50 }
41 }
51 }
42 w.messages = append(w.messages, msg)
52 w.messages = append(w.messages, msg)
internal/lsp/client.go+14−6
@@ -6,7 +6,6 @@ import (
6 "encoding/json"
6 "encoding/json"
7 "fmt"
7 "fmt"
8 "io"
8 "io"
9− "log"
10 "os"
9 "os"
11 "os/exec"
10 "os/exec"
12 "strings"
11 "strings"
@@ -14,9 +13,13 @@ import (
14 "sync/atomic"
13 "sync/atomic"
15 "time"
14 "time"
16
15
16+ "github.com/kujtimiihoxha/termai/internal/config"
17+ "github.com/kujtimiihoxha/termai/internal/logging"
17 "github.com/kujtimiihoxha/termai/internal/lsp/protocol"
18 "github.com/kujtimiihoxha/termai/internal/lsp/protocol"
18)
19)
19
20
21+var logger = logging.Get()
22+
20type Client struct {
23type Client struct {
21 Cmd *exec.Cmd
24 Cmd *exec.Cmd
22 stdin io.WriteCloser
25 stdin io.WriteCloser
@@ -357,6 +360,7 @@ func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
357}
360}
358
361
359func (c *Client) CloseFile(ctx context.Context, filepath string) error {
362func (c *Client) CloseFile(ctx context.Context, filepath string) error {
363+ cnf := config.Get()
360 uri := fmt.Sprintf("file://%s", filepath)
364 uri := fmt.Sprintf("file://%s", filepath)
361
365
362 c.openFilesMu.Lock()
366 c.openFilesMu.Lock()
@@ -371,7 +375,10 @@ func (c *Client) CloseFile(ctx context.Context, filepath string) error {
371 URI: protocol.DocumentUri(uri),
375 URI: protocol.DocumentUri(uri),
372 },
376 },
373 }
377 }
374− log.Println("Closing", params.TextDocument.URI.Dir())
378+
379+ if cnf.Debug {
380+ logger.Debug("Closing file", "file", filepath)
381+ }
375 if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
382 if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
376 return err
383 return err
377 }
384 }
@@ -393,6 +400,7 @@ func (c *Client) IsFileOpen(filepath string) bool {
393
400
394// CloseAllFiles closes all currently open files
401// CloseAllFiles closes all currently open files
395func (c *Client) CloseAllFiles(ctx context.Context) {
402func (c *Client) CloseAllFiles(ctx context.Context) {
403+ cnf := config.Get()
396 c.openFilesMu.Lock()
404 c.openFilesMu.Lock()
397 filesToClose := make([]string, 0, len(c.openFiles))
405 filesToClose := make([]string, 0, len(c.openFiles))
398
406
@@ -407,13 +415,13 @@ func (c *Client) CloseAllFiles(ctx context.Context) {
407 // Then close them all
415 // Then close them all
408 for _, filePath := range filesToClose {
416 for _, filePath := range filesToClose {
409 err := c.CloseFile(ctx, filePath)
417 err := c.CloseFile(ctx, filePath)
410− if err != nil && debug {
418+ if err != nil && cnf.Debug {
411− log.Printf("Error closing file %s: %v", filePath, err)
419+ logger.Warn("Error closing file", "file", filePath, "error", err)
412 }
420 }
413 }
421 }
414
422
415− if debug {
423+ if cnf.Debug {
416− log.Printf("Closed %d files", len(filesToClose))
424+ logger.Debug("Closed all files", "files", filesToClose)
417 }
425 }
418}
426}
419
427
internal/lsp/handlers.go+10−7
@@ -2,8 +2,8 @@ package lsp
2
2
3import (
3import (
4 "encoding/json"
4 "encoding/json"
5− "log"
6
5
6+ "github.com/kujtimiihoxha/termai/internal/config"
7 "github.com/kujtimiihoxha/termai/internal/lsp/protocol"
7 "github.com/kujtimiihoxha/termai/internal/lsp/protocol"
8 "github.com/kujtimiihoxha/termai/internal/lsp/util"
8 "github.com/kujtimiihoxha/termai/internal/lsp/util"
9)
9)
@@ -17,7 +17,7 @@ func HandleWorkspaceConfiguration(params json.RawMessage) (any, error) {
17func HandleRegisterCapability(params json.RawMessage) (any, error) {
17func HandleRegisterCapability(params json.RawMessage) (any, error) {
18 var registerParams protocol.RegistrationParams
18 var registerParams protocol.RegistrationParams
19 if err := json.Unmarshal(params, &registerParams); err != nil {
19 if err := json.Unmarshal(params, &registerParams); err != nil {
20− log.Printf("Error unmarshaling registration params: %v", err)
20+ logger.Error("Error unmarshaling registration params", "error", err)
21 return nil, err
21 return nil, err
22 }
22 }
23
23
@@ -27,13 +27,13 @@ func HandleRegisterCapability(params json.RawMessage) (any, error) {
27 // Parse the registration options
27 // Parse the registration options
28 optionsJSON, err := json.Marshal(reg.RegisterOptions)
28 optionsJSON, err := json.Marshal(reg.RegisterOptions)
29 if err != nil {
29 if err != nil {
30− log.Printf("Error marshaling registration options: %v", err)
30+ logger.Error("Error marshaling registration options", "error", err)
31 continue
31 continue
32 }
32 }
33
33
34 var options protocol.DidChangeWatchedFilesRegistrationOptions
34 var options protocol.DidChangeWatchedFilesRegistrationOptions
35 if err := json.Unmarshal(optionsJSON, &options); err != nil {
35 if err := json.Unmarshal(optionsJSON, &options); err != nil {
36− log.Printf("Error unmarshaling registration options: %v", err)
36+ logger.Error("Error unmarshaling registration options", "error", err)
37 continue
37 continue
38 }
38 }
39
39
@@ -53,7 +53,7 @@ func HandleApplyEdit(params json.RawMessage) (any, error) {
53
53
54 err := util.ApplyWorkspaceEdit(edit.Edit)
54 err := util.ApplyWorkspaceEdit(edit.Edit)
55 if err != nil {
55 if err != nil {
56− log.Printf("Error applying workspace edit: %v", err)
56+ logger.Error("Error applying workspace edit", "error", err)
57 return protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: err.Error()}, nil
57 return protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: err.Error()}, nil
58 }
58 }
59
59
@@ -81,19 +81,22 @@ func notifyFileWatchRegistration(id string, watchers []protocol.FileSystemWatche
81// Notifications
81// Notifications
82
82
83func HandleServerMessage(params json.RawMessage) {
83func HandleServerMessage(params json.RawMessage) {
84+ cnf := config.Get()
84 var msg struct {
85 var msg struct {
85 Type int `json:"type"`
86 Type int `json:"type"`
86 Message string `json:"message"`
87 Message string `json:"message"`
87 }
88 }
88 if err := json.Unmarshal(params, &msg); err == nil {
89 if err := json.Unmarshal(params, &msg); err == nil {
90+ if cnf.Debug {
89− log.Printf("Server message: %s\n", msg.Message)
91+ logger.Debug("Server message", "type", msg.Type, "message", msg.Message)
92+ }
90 }
93 }
91}
94}
92
95
93func HandleDiagnostics(client *Client, params json.RawMessage) {
96func HandleDiagnostics(client *Client, params json.RawMessage) {
94 var diagParams protocol.PublishDiagnosticsParams
97 var diagParams protocol.PublishDiagnosticsParams
95 if err := json.Unmarshal(params, &diagParams); err != nil {
98 if err := json.Unmarshal(params, &diagParams); err != nil {
96− log.Printf("Error unmarshaling diagnostic params: %v", err)
99+ logger.Error("Error unmarshaling diagnostics params", "error", err)
97 return
100 return
98 }
101 }
99
102
internal/lsp/transport.go+36−34
@@ -6,12 +6,10 @@ import (
6 "encoding/json"
6 "encoding/json"
7 "fmt"
7 "fmt"
8 "io"
8 "io"
9− "log"
10− "os"
11 "strings"
9 "strings"
12−)
13
10
14−var debug = os.Getenv("DEBUG") != ""
11+ "github.com/kujtimiihoxha/termai/internal/config"
12+)
15
13
16// Write writes an LSP message to the given writer
14// Write writes an LSP message to the given writer
17func WriteMessage(w io.Writer, msg *Message) error {
15func WriteMessage(w io.Writer, msg *Message) error {
@@ -19,10 +17,10 @@ func WriteMessage(w io.Writer, msg *Message) error {
19 if err != nil {
17 if err != nil {
20 return fmt.Errorf("failed to marshal message: %w", err)
18 return fmt.Errorf("failed to marshal message: %w", err)
21 }
19 }
20+ cnf := config.Get()
22
21
23− if debug {
22+ if cnf.Debug {
24− log.Printf("%v", msg.Method)
23+ logger.Debug("Sending message to server", "method", msg.Method, "id", msg.ID)
25− log.Printf("-> Sending: %s", string(data))
26 }
24 }
27
25
28 _, err = fmt.Fprintf(w, "Content-Length: %d\r\n\r\n", len(data))
26 _, 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 {
40
38
41// ReadMessage reads a single LSP message from the given reader
39// ReadMessage reads a single LSP message from the given reader
42func ReadMessage(r *bufio.Reader) (*Message, error) {
40func ReadMessage(r *bufio.Reader) (*Message, error) {
41+ cnf := config.Get()
43 // Read headers
42 // Read headers
44 var contentLength int
43 var contentLength int
45 for {
44 for {
@@ -49,8 +48,8 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
49 }
48 }
50 line = strings.TrimSpace(line)
49 line = strings.TrimSpace(line)
51
50
52− if debug {
51+ if cnf.Debug {
53− log.Printf("<- Header: %s", line)
52+ logger.Debug("Received header", "line", line)
54 }
53 }
55
54
56 if line == "" {
55 if line == "" {
@@ -65,8 +64,8 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
65 }
64 }
66 }
65 }
67
66
68− if debug {
67+ if cnf.Debug {
69− log.Printf("<- Reading content with length: %d", contentLength)
68+ logger.Debug("Content-Length", "length", contentLength)
70 }
69 }
71
70
72 // Read content
71 // Read content
@@ -76,8 +75,8 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
76 return nil, fmt.Errorf("failed to read content: %w", err)
75 return nil, fmt.Errorf("failed to read content: %w", err)
77 }
76 }
78
77
79− if debug {
78+ if cnf.Debug {
80− log.Printf("<- Received: %s", string(content))
79+ logger.Debug("Received content", "content", string(content))
81 }
80 }
82
81
83 // Parse message
82 // Parse message
@@ -91,19 +90,20 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
91
90
92// handleMessages reads and dispatches messages in a loop
91// handleMessages reads and dispatches messages in a loop
93func (c *Client) handleMessages() {
92func (c *Client) handleMessages() {
93+ cnf := config.Get()
94 for {
94 for {
95 msg, err := ReadMessage(c.stdout)
95 msg, err := ReadMessage(c.stdout)
96 if err != nil {
96 if err != nil {
97− if debug {
97+ if cnf.Debug {
98− log.Printf("Error reading message: %v", err)
98+ logger.Error("Error reading message", "error", err)
99 }
99 }
100 return
100 return
101 }
101 }
102
102
103 // Handle server->client request (has both Method and ID)
103 // Handle server->client request (has both Method and ID)
104 if msg.Method != "" && msg.ID != 0 {
104 if msg.Method != "" && msg.ID != 0 {
105− if debug {
105+ if cnf.Debug {
106− log.Printf("Received request from server: method=%s id=%d", msg.Method, msg.ID)
106+ logger.Debug("Received request from server", "method", msg.Method, "id", msg.ID)
107 }
107 }
108
108
109 response := &Message{
109 response := &Message{
@@ -143,7 +143,7 @@ func (c *Client) handleMessages() {
143
143
144 // Send response back to server
144 // Send response back to server
145 if err := WriteMessage(c.stdin, response); err != nil {
145 if err := WriteMessage(c.stdin, response); err != nil {
146− log.Printf("Error sending response to server: %v", err)
146+ logger.Error("Error sending response to server", "error", err)
147 }
147 }
148
148
149 continue
149 continue
@@ -156,12 +156,12 @@ func (c *Client) handleMessages() {
156 c.notificationMu.RUnlock()
156 c.notificationMu.RUnlock()
157
157
158 if ok {
158 if ok {
159− if debug {
159+ if cnf.Debug {
160− log.Printf("Handling notification: %s", msg.Method)
160+ logger.Debug("Handling notification", "method", msg.Method)
161 }
161 }
162 go handler(msg.Params)
162 go handler(msg.Params)
163− } else if debug {
163+ } else if cnf.Debug {
164− log.Printf("No handler for notification: %s", msg.Method)
164+ logger.Debug("No handler for notification", "method", msg.Method)
165 }
165 }
166 continue
166 continue
167 }
167 }
@@ -173,13 +173,13 @@ func (c *Client) handleMessages() {
173 c.handlersMu.RUnlock()
173 c.handlersMu.RUnlock()
174
174
175 if ok {
175 if ok {
176− if debug {
176+ if cnf.Debug {
177− log.Printf("Sending response for ID %d to handler", msg.ID)
177+ logger.Debug("Received response for request", "id", msg.ID)
178 }
178 }
179 ch <- msg
179 ch <- msg
180 close(ch)
180 close(ch)
181− } else if debug {
181+ } else if cnf.Debug {
182− log.Printf("No handler for response ID: %d", msg.ID)
182+ logger.Debug("No handler for response", "id", msg.ID)
183 }
183 }
184 }
184 }
185 }
185 }
@@ -187,10 +187,11 @@ func (c *Client) handleMessages() {
187
187
188// Call makes a request and waits for the response
188// Call makes a request and waits for the response
189func (c *Client) Call(ctx context.Context, method string, params any, result any) error {
189func (c *Client) Call(ctx context.Context, method string, params any, result any) error {
190+ cnf := config.Get()
190 id := c.nextID.Add(1)
191 id := c.nextID.Add(1)
191
192
192− if debug {
193+ if cnf.Debug {
193− log.Printf("Making call: method=%s id=%d", method, id)
194+ logger.Debug("Making call", "method", method, "id", id)
194 }
195 }
195
196
196 msg, err := NewRequest(id, method, params)
197 msg, err := NewRequest(id, method, params)
@@ -215,15 +216,15 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
215 return fmt.Errorf("failed to send request: %w", err)
216 return fmt.Errorf("failed to send request: %w", err)
216 }
217 }
217
218
218− if debug {
219+ if cnf.Debug {
219− log.Printf("Waiting for response to request ID: %d", id)
220+ logger.Debug("Request sent", "method", method, "id", id)
220 }
221 }
221
222
222 // Wait for response
223 // Wait for response
223 resp := <-ch
224 resp := <-ch
224
225
225− if debug {
226+ if cnf.Debug {
226− log.Printf("Received response for request ID: %d", id)
227+ logger.Debug("Received response", "id", id)
227 }
228 }
228
229
229 if resp.Error != nil {
230 if resp.Error != nil {
@@ -247,8 +248,9 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
247
248
248// Notify sends a notification (a request without an ID that doesn't expect a response)
249// Notify sends a notification (a request without an ID that doesn't expect a response)
249func (c *Client) Notify(ctx context.Context, method string, params any) error {
250func (c *Client) Notify(ctx context.Context, method string, params any) error {
250− if debug {
251+ cnf := config.Get()
252+ if cnf.Debug {
251− log.Printf("Sending notification: method=%s", method)
253+ logger.Debug("Sending notification", "method", method)
252 }
254 }
253
255
254 msg, err := NewNotification(method, params)
256 msg, err := NewNotification(method, params)
internal/lsp/watcher/watcher.go+71−47
@@ -3,7 +3,6 @@ package watcher
3import (
3import (
4 "context"
4 "context"
5 "fmt"
5 "fmt"
6− "log"
7 "os"
6 "os"
8 "path/filepath"
7 "path/filepath"
9 "strings"
8 "strings"
@@ -11,11 +10,13 @@ import (
11 "time"
10 "time"
12
11
13 "github.com/fsnotify/fsnotify"
12 "github.com/fsnotify/fsnotify"
13+ "github.com/kujtimiihoxha/termai/internal/config"
14+ "github.com/kujtimiihoxha/termai/internal/logging"
14 "github.com/kujtimiihoxha/termai/internal/lsp"
15 "github.com/kujtimiihoxha/termai/internal/lsp"
15 "github.com/kujtimiihoxha/termai/internal/lsp/protocol"
16 "github.com/kujtimiihoxha/termai/internal/lsp/protocol"
16)
17)
17
18
18−var debug = false // Force debug logging on
19+var logger = logging.Get()
19
20
20// WorkspaceWatcher manages LSP file watching
21// WorkspaceWatcher manages LSP file watching
21type WorkspaceWatcher struct {
22type WorkspaceWatcher struct {
@@ -43,6 +44,7 @@ func NewWorkspaceWatcher(client *lsp.Client) *WorkspaceWatcher {
43
44
44// AddRegistrations adds file watchers to track
45// AddRegistrations adds file watchers to track
45func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watchers []protocol.FileSystemWatcher) {
46func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watchers []protocol.FileSystemWatcher) {
47+ cnf := config.Get()
46 w.registrationMu.Lock()
48 w.registrationMu.Lock()
47 defer w.registrationMu.Unlock()
49 defer w.registrationMu.Unlock()
48
50
@@ -50,31 +52,35 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
50 w.registrations = append(w.registrations, watchers...)
52 w.registrations = append(w.registrations, watchers...)
51
53
52 // Print detailed registration information for debugging
54 // Print detailed registration information for debugging
53− if debug {
55+ if cnf.Debug {
54− log.Printf("Added %d file watcher registrations (id: %s), total: %d",
56+ logger.Debug("Adding file watcher registrations",
57+ "id", id,
58+ "watchers", len(watchers),
55− len(watchers), id, len(w.registrations))
59+ "total", len(w.registrations),
60+ "watchers", watchers,
61+ )
56
62
57 for i, watcher := range watchers {
63 for i, watcher := range watchers {
58− log.Printf("Registration #%d raw data:", i+1)
64+ logger.Debug("Registration", "index", i+1)
59
65
60 // Log the GlobPattern
66 // Log the GlobPattern
61 switch v := watcher.GlobPattern.Value.(type) {
67 switch v := watcher.GlobPattern.Value.(type) {
62 case string:
68 case string:
63− log.Printf(" GlobPattern: string pattern '%s'", v)
69+ logger.Debug("GlobPattern", "pattern", v)
64 case protocol.RelativePattern:
70 case protocol.RelativePattern:
65− log.Printf(" GlobPattern: RelativePattern with pattern '%s'", v.Pattern)
71+ logger.Debug("GlobPattern", "pattern", v.Pattern)
66
72
67 // Log BaseURI details
73 // Log BaseURI details
68 switch u := v.BaseURI.Value.(type) {
74 switch u := v.BaseURI.Value.(type) {
69 case string:
75 case string:
70− log.Printf(" BaseURI: string '%s'", u)
76+ logger.Debug("BaseURI", "baseURI", u)
71 case protocol.DocumentUri:
77 case protocol.DocumentUri:
72− log.Printf(" BaseURI: DocumentUri '%s'", u)
78+ logger.Debug("BaseURI", "baseURI", u)
73 default:
79 default:
74− log.Printf(" BaseURI: unknown type %T", u)
80+ logger.Debug("BaseURI", "baseURI", u)
75 }
81 }
76 default:
82 default:
77− log.Printf(" GlobPattern: unknown type %T", v)
83+ logger.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v))
78 }
84 }
79
85
80 // Log WatchKind
86 // Log WatchKind
@@ -82,11 +88,8 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
82 if watcher.Kind != nil {
88 if watcher.Kind != nil {
83 watchKind = *watcher.Kind
89 watchKind = *watcher.Kind
84 }
90 }
85− log.Printf(" WatchKind: %d (Create:%v, Change:%v, Delete:%v)",
91+
86− watchKind,
92+ logger.Debug("WatchKind", "kind", watchKind)
87− watchKind&protocol.WatchCreate != 0,
88− watchKind&protocol.WatchChange != 0,
89− watchKind&protocol.WatchDelete != 0)
90
93
91 // Test match against some example paths
94 // Test match against some example paths
92 testPaths := []string{
95 testPaths := []string{
@@ -96,7 +99,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
96
99
97 for _, testPath := range testPaths {
100 for _, testPath := range testPaths {
98 isMatch := w.matchesPattern(testPath, watcher.GlobPattern)
101 isMatch := w.matchesPattern(testPath, watcher.GlobPattern)
99− log.Printf(" Test path '%s': %v", testPath, isMatch)
102+ logger.Debug("Test path", "path", testPath, "matches", isMatch)
100 }
103 }
101 }
104 }
102 }
105 }
@@ -114,10 +117,9 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
114
117
115 // Skip directories that should be excluded
118 // Skip directories that should be excluded
116 if d.IsDir() {
119 if d.IsDir() {
117− log.Println(path)
118 if path != w.workspacePath && shouldExcludeDir(path) {
120 if path != w.workspacePath && shouldExcludeDir(path) {
119− if debug {
121+ if cnf.Debug {
120− log.Printf("Skipping excluded directory!!: %s", path)
122+ logger.Debug("Skipping excluded directory", "path", path)
121 }
123 }
122 return filepath.SkipDir
124 return filepath.SkipDir
123 }
125 }
@@ -136,18 +138,23 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
136 })
138 })
137
139
138 elapsedTime := time.Since(startTime)
140 elapsedTime := time.Since(startTime)
139− if debug {
141+ if cnf.Debug {
140− log.Printf("Workspace scan complete: processed %d files in %.2f seconds", filesOpened, elapsedTime.Seconds())
142+ logger.Debug("Workspace scan complete",
143+ "filesOpened", filesOpened,
144+ "elapsedTime", elapsedTime.Seconds(),
145+ "workspacePath", w.workspacePath,
146+ )
141 }
147 }
142
148
143− if err != nil && debug {
149+ if err != nil && cnf.Debug {
144− log.Printf("Error scanning workspace for files to open: %v", err)
150+ logger.Debug("Error scanning workspace for files to open", "error", err)
145 }
151 }
146 }()
152 }()
147}
153}
148
154
149// WatchWorkspace sets up file watching for a workspace
155// WatchWorkspace sets up file watching for a workspace
150func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath string) {
156func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath string) {
157+ cnf := config.Get()
151 w.workspacePath = workspacePath
158 w.workspacePath = workspacePath
152
159
153 // Register handler for file watcher registrations from the server
160 // Register handler for file watcher registrations from the server
@@ -157,7 +164,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
157
164
158 watcher, err := fsnotify.NewWatcher()
165 watcher, err := fsnotify.NewWatcher()
159 if err != nil {
166 if err != nil {
160− log.Fatalf("Error creating watcher: %v", err)
167+ logger.Error("Error creating watcher", "error", err)
161 }
168 }
162 defer watcher.Close()
169 defer watcher.Close()
163
170
@@ -170,8 +177,8 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
170 // Skip excluded directories (except workspace root)
177 // Skip excluded directories (except workspace root)
171 if d.IsDir() && path != workspacePath {
178 if d.IsDir() && path != workspacePath {
172 if shouldExcludeDir(path) {
179 if shouldExcludeDir(path) {
173− if debug {
180+ if cnf.Debug {
174− log.Printf("Skipping watching excluded directory: %s", path)
181+ logger.Debug("Skipping excluded directory", "path", path)
175 }
182 }
176 return filepath.SkipDir
183 return filepath.SkipDir
177 }
184 }
@@ -181,14 +188,14 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
181 if d.IsDir() {
188 if d.IsDir() {
182 err = watcher.Add(path)
189 err = watcher.Add(path)
183 if err != nil {
190 if err != nil {
184− log.Printf("Error watching path %s: %v", path, err)
191+ logger.Error("Error watching path", "path", path, "error", err)
185 }
192 }
186 }
193 }
187
194
188 return nil
195 return nil
189 })
196 })
190 if err != nil {
197 if err != nil {
191− log.Fatalf("Error walking workspace: %v", err)
198+ logger.Error("Error walking workspace", "error", err)
192 }
199 }
193
200
194 // Event loop
201 // Event loop
@@ -210,7 +217,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
210 // Skip excluded directories
217 // Skip excluded directories
211 if !shouldExcludeDir(event.Name) {
218 if !shouldExcludeDir(event.Name) {
212 if err := watcher.Add(event.Name); err != nil {
219 if err := watcher.Add(event.Name); err != nil {
213− log.Printf("Error watching new directory: %v", err)
220+ logger.Error("Error adding directory to watcher", "path", event.Name, "error", err)
214 }
221 }
215 }
222 }
216 } else {
223 } else {
@@ -223,10 +230,15 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
223 }
230 }
224
231
225 // Debug logging
232 // Debug logging
226− if debug {
233+ if cnf.Debug {
227 matched, kind := w.isPathWatched(event.Name)
234 matched, kind := w.isPathWatched(event.Name)
228− log.Printf("Event: %s, Op: %s, Watched: %v, Kind: %d",
235+ logger.Debug("File event",
236+ "path", event.Name,
229− event.Name, event.Op.String(), matched, kind)
237+ "operation", event.Op.String(),
238+ "watched", matched,
239+ "kind", kind,
240+ )
241+
230 }
242 }
231
243
232 // Check if this path should be watched according to server registrations
244 // Check if this path should be watched according to server registrations
@@ -265,7 +277,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
265 if !ok {
277 if !ok {
266 return
278 return
267 }
279 }
268− log.Printf("Watcher error: %v\n", err)
280+ logger.Error("Error watching file", "error", err)
269 }
281 }
270 }
282 }
271}
283}
@@ -390,7 +402,7 @@ func matchesSimpleGlob(pattern, path string) bool {
390 // Fall back to simple matching for simpler patterns
402 // Fall back to simple matching for simpler patterns
391 matched, err := filepath.Match(pattern, path)
403 matched, err := filepath.Match(pattern, path)
392 if err != nil {
404 if err != nil {
393− log.Printf("Error matching pattern %s: %v", pattern, err)
405+ logger.Error("Error matching pattern", "pattern", pattern, "path", path, "error", err)
394 return false
406 return false
395 }
407 }
396
408
@@ -401,7 +413,7 @@ func matchesSimpleGlob(pattern, path string) bool {
401func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPattern) bool {
413func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPattern) bool {
402 patternInfo, err := pattern.AsPattern()
414 patternInfo, err := pattern.AsPattern()
403 if err != nil {
415 if err != nil {
404− log.Printf("Error parsing pattern: %v", err)
416+ logger.Error("Error parsing pattern", "pattern", pattern, "error", err)
405 return false
417 return false
406 }
418 }
407
419
@@ -426,7 +438,7 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt
426 // Make path relative to basePath for matching
438 // Make path relative to basePath for matching
427 relPath, err := filepath.Rel(basePath, path)
439 relPath, err := filepath.Rel(basePath, path)
428 if err != nil {
440 if err != nil {
429− log.Printf("Error getting relative path for %s: %v", path, err)
441+ logger.Error("Error getting relative path", "path", path, "basePath", basePath, "error", err)
430 return false
442 return false
431 }
443 }
432 relPath = filepath.ToSlash(relPath)
444 relPath = filepath.ToSlash(relPath)
@@ -467,21 +479,25 @@ func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, chan
467 if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
479 if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
468 err := w.client.NotifyChange(ctx, filePath)
480 err := w.client.NotifyChange(ctx, filePath)
469 if err != nil {
481 if err != nil {
470− log.Printf("Error notifying change: %v", err)
482+ logger.Error("Error notifying change", "error", err)
471 }
483 }
472 return
484 return
473 }
485 }
474
486
475 // Notify LSP server about the file event using didChangeWatchedFiles
487 // Notify LSP server about the file event using didChangeWatchedFiles
476 if err := w.notifyFileEvent(ctx, uri, changeType); err != nil {
488 if err := w.notifyFileEvent(ctx, uri, changeType); err != nil {
477− log.Printf("Error notifying LSP server about file event: %v", err)
489+ logger.Error("Error notifying LSP server about file event", "error", err)
478 }
490 }
479}
491}
480
492
481// notifyFileEvent sends a didChangeWatchedFiles notification for a file event
493// notifyFileEvent sends a didChangeWatchedFiles notification for a file event
482func (w *WorkspaceWatcher) notifyFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) error {
494func (w *WorkspaceWatcher) notifyFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) error {
483− if debug {
495+ cnf := config.Get()
496+ if cnf.Debug {
484− log.Printf("Notifying file event: %s (type: %d)", uri, changeType)
497+ logger.Debug("Notifying file event",
498+ "uri", uri,
499+ "changeType", changeType,
500+ )
485 }
501 }
486
502
487 params := protocol.DidChangeWatchedFilesParams{
503 params := protocol.DidChangeWatchedFilesParams{
@@ -575,7 +591,7 @@ func shouldExcludeDir(dirPath string) bool {
575// shouldExcludeFile returns true if the file should be excluded from opening
591// shouldExcludeFile returns true if the file should be excluded from opening
576func shouldExcludeFile(filePath string) bool {
592func shouldExcludeFile(filePath string) bool {
577 fileName := filepath.Base(filePath)
593 fileName := filepath.Base(filePath)
578−
594+ cnf := config.Get()
579 // Skip dot files
595 // Skip dot files
580 if strings.HasPrefix(fileName, ".") {
596 if strings.HasPrefix(fileName, ".") {
581 return true
597 return true
@@ -601,8 +617,15 @@ func shouldExcludeFile(filePath string) bool {
601
617
602 // Skip large files
618 // Skip large files
603 if info.Size() > maxFileSize {
619 if info.Size() > maxFileSize {
604− if debug {
620+ if cnf.Debug {
621+ logger.Debug("Skipping large file",
622+ "path", filePath,
623+ "size", info.Size(),
624+ "maxSize", maxFileSize,
625+ "debug", cnf.Debug,
605− log.Printf("Skipping large file: %s (%.2f MB)", filePath, float64(info.Size())/(1024*1024))
626+ "sizeMB", float64(info.Size())/(1024*1024),
627+ "maxSizeMB", float64(maxFileSize)/(1024*1024),
628+ )
606 }
629 }
607 return true
630 return true
608 }
631 }
@@ -612,6 +635,7 @@ func shouldExcludeFile(filePath string) bool {
612
635
613// openMatchingFile opens a file if it matches any of the registered patterns
636// openMatchingFile opens a file if it matches any of the registered patterns
614func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
637func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
638+ cnf := config.Get()
615 // Skip directories
639 // Skip directories
616 info, err := os.Stat(path)
640 info, err := os.Stat(path)
617 if err != nil || info.IsDir() {
641 if err != nil || info.IsDir() {
@@ -626,8 +650,8 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
626 // Check if this path should be watched according to server registrations
650 // Check if this path should be watched according to server registrations
627 if watched, _ := w.isPathWatched(path); watched {
651 if watched, _ := w.isPathWatched(path); watched {
628 // Don't need to check if it's already open - the client.OpenFile handles that
652 // Don't need to check if it's already open - the client.OpenFile handles that
629− if err := w.client.OpenFile(ctx, path); err != nil && debug {
653+ if err := w.client.OpenFile(ctx, path); err != nil && cnf.Debug {
630− log.Printf("Error opening file %s: %v", path, err)
654+ logger.Error("Error opening file", "path", path, "error", err)
631 }
655 }
632 }
656 }
633}
657}
internal/tui/components/core/status.go+15−9
@@ -7,7 +7,6 @@ import (
7 "github.com/charmbracelet/lipgloss"
7 "github.com/charmbracelet/lipgloss"
8 "github.com/kujtimiihoxha/termai/internal/config"
8 "github.com/kujtimiihoxha/termai/internal/config"
9 "github.com/kujtimiihoxha/termai/internal/llm/models"
9 "github.com/kujtimiihoxha/termai/internal/llm/models"
10− "github.com/kujtimiihoxha/termai/internal/pubsub"
11 "github.com/kujtimiihoxha/termai/internal/tui/styles"
10 "github.com/kujtimiihoxha/termai/internal/tui/styles"
12 "github.com/kujtimiihoxha/termai/internal/tui/util"
11 "github.com/kujtimiihoxha/termai/internal/tui/util"
13 "github.com/kujtimiihoxha/termai/internal/version"
12 "github.com/kujtimiihoxha/termai/internal/version"
@@ -20,8 +19,8 @@ type statusCmp struct {
20}
19}
21
20
22// clearMessageCmd is a command that clears status messages after a timeout
21// clearMessageCmd is a command that clears status messages after a timeout
23−func (m statusCmp) clearMessageCmd() tea.Cmd {
22+func (m statusCmp) clearMessageCmd(ttl time.Duration) tea.Cmd {
24− return tea.Tick(m.messageTTL, func(time.Time) tea.Msg {
23+ return tea.Tick(ttl, func(time.Time) tea.Msg {
25 return util.ClearStatusMsg{}
24 return util.ClearStatusMsg{}
26 })
25 })
27}
26}
@@ -34,13 +33,14 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
34 switch msg := msg.(type) {
33 switch msg := msg.(type) {
35 case tea.WindowSizeMsg:
34 case tea.WindowSizeMsg:
36 m.width = msg.Width
35 m.width = msg.Width
37− return m, m.clearMessageCmd()
36+ return m, nil
38− case pubsub.Event[util.InfoMsg]:
39− m.info = &msg.Payload
40− return m, m.clearMessageCmd()
41 case util.InfoMsg:
37 case util.InfoMsg:
42 m.info = &msg
38 m.info = &msg
39+ ttl := msg.TTL
40+ if ttl == 0 {
41+ ttl = m.messageTTL
42+ }
43− return m, m.clearMessageCmd()
43+ return m, m.clearMessageCmd(ttl)
44 case util.ClearStatusMsg:
44 case util.ClearStatusMsg:
45 m.info = nil
45 m.info = nil
46 }
46 }
@@ -66,7 +66,13 @@ func (m statusCmp) View() string {
66 case util.InfoTypeError:
66 case util.InfoTypeError:
67 infoStyle = infoStyle.Background(styles.Red)
67 infoStyle = infoStyle.Background(styles.Red)
68 }
68 }
69+ // Truncate message if it's longer than available width
70+ msg := m.info.Msg
71+ availWidth := m.availableFooterMsgWidth() - 3 // Account for ellipsis
72+ if len(msg) > availWidth && availWidth > 0 {
73+ msg = msg[:availWidth] + "..."
74+ }
69− status += infoStyle.Render(m.info.Msg)
75+ status += infoStyle.Render(msg)
70 } else {
76 } else {
71 status += styles.Padded.
77 status += styles.Padded.
72 Foreground(styles.Base).
78 Foreground(styles.Base).
internal/tui/components/logs/details.gonew file+176
@@ -0,0 +1,176 @@
1+package logs
2+
3+import (
4+ "fmt"
5+ "strings"
6+ "time"
7+
8+ "github.com/charmbracelet/bubbles/key"
9+ "github.com/charmbracelet/bubbles/viewport"
10+ tea "github.com/charmbracelet/bubbletea"
11+ "github.com/charmbracelet/lipgloss"
12+ "github.com/kujtimiihoxha/termai/internal/logging"
13+ "github.com/kujtimiihoxha/termai/internal/tui/layout"
14+ "github.com/kujtimiihoxha/termai/internal/tui/styles"
15+)
16+
17+type DetailComponent interface {
18+ tea.Model
19+ layout.Focusable
20+ layout.Sizeable
21+ layout.Bindings
22+ layout.Bordered
23+}
24+
25+type detailCmp struct {
26+ width, height int
27+ focused bool
28+ currentLog logging.LogMessage
29+ viewport viewport.Model
30+}
31+
32+func (i *detailCmp) Init() tea.Cmd {
33+ messages := logging.Get().List()
34+ if len(messages) == 0 {
35+ return nil
36+ }
37+ i.currentLog = messages[0]
38+ return nil
39+}
40+
41+func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
42+ var (
43+ cmd tea.Cmd
44+ cmds []tea.Cmd
45+ )
46+
47+ switch msg := msg.(type) {
48+ case selectedLogMsg:
49+ if msg.ID != i.currentLog.ID {
50+ i.currentLog = logging.LogMessage(msg)
51+ i.updateContent()
52+ }
53+ }
54+
55+ if i.focused {
56+ i.viewport, cmd = i.viewport.Update(msg)
57+ cmds = append(cmds, cmd)
58+ }
59+
60+ return i, tea.Batch(cmds...)
61+}
62+
63+func (i *detailCmp) updateContent() {
64+ var content strings.Builder
65+
66+ // Format the header with timestamp and level
67+ timeStyle := lipgloss.NewStyle().Foreground(styles.SubText0)
68+ levelStyle := getLevelStyle(i.currentLog.Level)
69+
70+ header := lipgloss.JoinHorizontal(
71+ lipgloss.Center,
72+ timeStyle.Render(i.currentLog.Time.Format(time.RFC3339)),
73+ " ",
74+ levelStyle.Render(i.currentLog.Level),
75+ )
76+
77+ content.WriteString(lipgloss.NewStyle().Bold(true).Render(header))
78+ content.WriteString("\n\n")
79+
80+ // Message with styling
81+ messageStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text)
82+ content.WriteString(messageStyle.Render("Message:"))
83+ content.WriteString("\n")
84+ content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(i.currentLog.Message))
85+ content.WriteString("\n\n")
86+
87+ // Attributes section
88+ if len(i.currentLog.Attributes) > 0 {
89+ attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text)
90+ content.WriteString(attrHeaderStyle.Render("Attributes:"))
91+ content.WriteString("\n")
92+
93+ // Create a table-like display for attributes
94+ keyStyle := lipgloss.NewStyle().Foreground(styles.Primary).Bold(true)
95+ valueStyle := lipgloss.NewStyle().Foreground(styles.Text)
96+
97+ for _, attr := range i.currentLog.Attributes {
98+ attrLine := fmt.Sprintf("%s: %s",
99+ keyStyle.Render(attr.Key),
100+ valueStyle.Render(attr.Value),
101+ )
102+ content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(attrLine))
103+ content.WriteString("\n")
104+ }
105+ }
106+
107+ i.viewport.SetContent(content.String())
108+}
109+
110+func getLevelStyle(level string) lipgloss.Style {
111+ style := lipgloss.NewStyle().Bold(true)
112+
113+ switch strings.ToLower(level) {
114+ case "info":
115+ return style.Foreground(styles.Blue)
116+ case "warn", "warning":
117+ return style.Foreground(styles.Warning)
118+ case "error", "err":
119+ return style.Foreground(styles.Error)
120+ case "debug":
121+ return style.Foreground(styles.Green)
122+ default:
123+ return style.Foreground(styles.Text)
124+ }
125+}
126+
127+func (i *detailCmp) View() string {
128+ return i.viewport.View()
129+}
130+
131+func (i *detailCmp) Blur() tea.Cmd {
132+ i.focused = false
133+ return nil
134+}
135+
136+func (i *detailCmp) Focus() tea.Cmd {
137+ i.focused = true
138+ return nil
139+}
140+
141+func (i *detailCmp) IsFocused() bool {
142+ return i.focused
143+}
144+
145+func (i *detailCmp) GetSize() (int, int) {
146+ return i.width, i.height
147+}
148+
149+func (i *detailCmp) SetSize(width int, height int) {
150+ i.width = width
151+ i.height = height
152+ i.viewport.Width = i.width
153+ i.viewport.Height = i.height
154+ i.updateContent()
155+}
156+
157+func (i *detailCmp) BindingKeys() []key.Binding {
158+ return []key.Binding{
159+ i.viewport.KeyMap.PageDown,
160+ i.viewport.KeyMap.PageUp,
161+ i.viewport.KeyMap.HalfPageDown,
162+ i.viewport.KeyMap.HalfPageUp,
163+ }
164+}
165+
166+func (i *detailCmp) BorderText() map[layout.BorderPosition]string {
167+ return map[layout.BorderPosition]string{
168+ layout.TopLeftBorder: "Log Details",
169+ }
170+}
171+
172+func NewLogsDetails() DetailComponent {
173+ return &detailCmp{
174+ viewport: viewport.New(0, 0),
175+ }
176+}
internal/tui/components/logs/table.go+27−11
@@ -11,6 +11,7 @@ import (
11 "github.com/kujtimiihoxha/termai/internal/pubsub"
11 "github.com/kujtimiihoxha/termai/internal/pubsub"
12 "github.com/kujtimiihoxha/termai/internal/tui/layout"
12 "github.com/kujtimiihoxha/termai/internal/tui/layout"
13 "github.com/kujtimiihoxha/termai/internal/tui/styles"
13 "github.com/kujtimiihoxha/termai/internal/tui/styles"
14+ "github.com/kujtimiihoxha/termai/internal/tui/util"
14)
15)
15
16
16type TableComponent interface {
17type TableComponent interface {
@@ -26,29 +27,42 @@ type tableCmp struct {
26 table table.Model
27 table table.Model
27}
28}
28
29
30+type selectedLogMsg logging.LogMessage
31+
29func (i *tableCmp) Init() tea.Cmd {
32func (i *tableCmp) Init() tea.Cmd {
30 i.setRows()
33 i.setRows()
31 return nil
34 return nil
32}
35}
33
36
34func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
37func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
38+ var cmds []tea.Cmd
35 if i.table.Focused() {
39 if i.table.Focused() {
36− switch msg := msg.(type) {
40+ switch msg.(type) {
37− case pubsub.Event[logging.Message]:
41+ case pubsub.Event[logging.LogMessage]:
38 i.setRows()
42 i.setRows()
39 return i, nil
43 return i, nil
40− case tea.KeyMsg:
41− if msg.String() == "ctrl+s" {
42− logger.Info("Saving logs...",
43− "rows", len(i.table.Rows()),
44− )
45− }
46 }
44 }
45+ prevSelectedRow := i.table.SelectedRow()
47 t, cmd := i.table.Update(msg)
46 t, cmd := i.table.Update(msg)
47+ cmds = append(cmds, cmd)
48 i.table = t
48 i.table = t
49− return i, cmd
49+ selectedRow := i.table.SelectedRow()
50+ if selectedRow != nil {
51+ if prevSelectedRow == nil || selectedRow[0] == prevSelectedRow[0] {
52+ var log logging.LogMessage
53+ for _, row := range logging.Get().List() {
54+ if row.ID == selectedRow[0] {
55+ log = row
56+ break
57+ }
58+ }
59+ if log.ID != "" {
60+ cmds = append(cmds, util.CmdHandler(selectedLogMsg(log)))
61+ }
62+ }
63+ }
50 }
64 }
51− return i, nil
65+ return i, tea.Batch(cmds...)
52}
66}
53
67
54func (i *tableCmp) View() string {
68func (i *tableCmp) View() string {
@@ -92,7 +106,7 @@ func (i *tableCmp) setRows() {
92 rows := []table.Row{}
106 rows := []table.Row{}
93
107
94 logs := logger.List()
108 logs := logger.List()
95− slices.SortFunc(logs, func(a, b logging.Message) int {
109+ slices.SortFunc(logs, func(a, b logging.LogMessage) int {
96 if a.Time.Before(b.Time) {
110 if a.Time.Before(b.Time) {
97 return 1
111 return 1
98 }
112 }
@@ -106,6 +120,7 @@ func (i *tableCmp) setRows() {
106 bm, _ := json.Marshal(log.Attributes)
120 bm, _ := json.Marshal(log.Attributes)
107
121
108 row := table.Row{
122 row := table.Row{
123+ log.ID,
109 log.Time.Format("15:04:05"),
124 log.Time.Format("15:04:05"),
110 log.Level,
125 log.Level,
111 log.Message,
126 log.Message,
@@ -118,6 +133,7 @@ func (i *tableCmp) setRows() {
118
133
119func NewLogsTable() TableComponent {
134func NewLogsTable() TableComponent {
120 columns := []table.Column{
135 columns := []table.Column{
136+ {Title: "ID", Width: 4},
121 {Title: "Time", Width: 4},
137 {Title: "Time", Width: 4},
122 {Title: "Level", Width: 10},
138 {Title: "Level", Width: 10},
123 {Title: "Message", Width: 10},
139 {Title: "Message", Width: 10},
internal/tui/layout/bento.go+38−17
@@ -187,7 +187,6 @@ func (b *bentoLayout) SetSize(width int, height int) {
187 b.width = width
187 b.width = width
188 b.height = height
188 b.height = height
189
189
190− // Check which panes are available
191 leftExists := false
190 leftExists := false
192 rightTopExists := false
191 rightTopExists := false
193 rightBottomExists := false
192 rightBottomExists := false
@@ -218,7 +217,6 @@ func (b *bentoLayout) SetSize(width int, height int) {
218 rightTopHeight = int(float64(height) * b.rightTopHeightRatio)
217 rightTopHeight = int(float64(height) * b.rightTopHeightRatio)
219 rightBottomHeight = height - rightTopHeight
218 rightBottomHeight = height - rightTopHeight
220
219
221− // Ensure minimum height for bottom pane
222 if rightBottomHeight < minRightBottomHeight && height >= minRightBottomHeight {
220 if rightBottomHeight < minRightBottomHeight && height >= minRightBottomHeight {
223 rightBottomHeight = minRightBottomHeight
221 rightBottomHeight = minRightBottomHeight
224 rightTopHeight = height - rightBottomHeight
222 rightTopHeight = height - rightBottomHeight
@@ -271,23 +269,47 @@ func (b *bentoLayout) HidePane(pane paneID) tea.Cmd {
271}
269}
272
270
273func (b *bentoLayout) SwitchPane(back bool) tea.Cmd {
271func (b *bentoLayout) SwitchPane(back bool) tea.Cmd {
272+ orderForward := []paneID{BentoLeftPane, BentoRightTopPane, BentoRightBottomPane}
273+ orderBackward := []paneID{BentoLeftPane, BentoRightBottomPane, BentoRightTopPane}
274+
275+ order := orderForward
274 if back {
276 if back {
275− switch b.currentPane {
277+ order = orderBackward
276− case BentoLeftPane:
278+ }
279+
280+ currentIdx := -1
281+ for i, id := range order {
277− b.currentPane = BentoRightBottomPane
282+ if id == b.currentPane {
278− case BentoRightTopPane:
283+ currentIdx = i
279− b.currentPane = BentoLeftPane
284+ break
280− case BentoRightBottomPane:
285+ }
286+ }
287+
288+ if currentIdx == -1 {
289+ for _, id := range order {
290+ if _, exists := b.panes[id]; exists {
291+ if _, hidden := b.hiddenPanes[id]; !hidden {
281− b.currentPane = BentoRightTopPane
292+ b.currentPane = id
293+ break
294+ }
295+ }
282 }
296 }
283 } else {
297 } else {
284− switch b.currentPane {
298+ startIdx := currentIdx
285− case BentoLeftPane:
299+ for {
286− b.currentPane = BentoRightTopPane
300+ currentIdx = (currentIdx + 1) % len(order)
287− case BentoRightTopPane:
301+
288− b.currentPane = BentoRightBottomPane
302+ nextID := order[currentIdx]
289− case BentoRightBottomPane:
303+ if _, exists := b.panes[nextID]; exists {
304+ if _, hidden := b.hiddenPanes[nextID]; !hidden {
290− b.currentPane = BentoLeftPane
305+ b.currentPane = nextID
306+ break
307+ }
308+ }
309+
310+ if currentIdx == startIdx {
311+ break
312+ }
291 }
313 }
292 }
314 }
293
315
@@ -319,7 +341,6 @@ type BentoLayoutOption func(*bentoLayout)
319func NewBentoLayout(panes BentoPanes, opts ...BentoLayoutOption) BentoLayout {
341func NewBentoLayout(panes BentoPanes, opts ...BentoLayoutOption) BentoLayout {
320 p := make(map[paneID]SinglePaneLayout, len(panes))
342 p := make(map[paneID]SinglePaneLayout, len(panes))
321 for id, pane := range panes {
343 for id, pane := range panes {
322− // Wrap any pane that is not a SinglePaneLayout in a SinglePaneLayout
323 if sp, ok := pane.(SinglePaneLayout); !ok {
344 if sp, ok := pane.(SinglePaneLayout); !ok {
324 p[id] = NewSinglePane(
345 p[id] = NewSinglePane(
325 pane,
346 pane,
internal/tui/page/logs.go+7−12
@@ -9,17 +9,12 @@ import (
9var LogsPage PageID = "logs"
9var LogsPage PageID = "logs"
10
10
11func NewLogsPage() tea.Model {
11func NewLogsPage() tea.Model {
12− p := layout.NewSinglePane(
12+ return layout.NewBentoLayout(
13+ layout.BentoPanes{
13− logs.NewLogsTable(),
14+ layout.BentoRightTopPane: logs.NewLogsTable(),
14− layout.WithSinglePaneFocusable(true),
15+ layout.BentoRightBottomPane: logs.NewLogsDetails(),
15− layout.WithSinglePaneBordered(true),
16− layout.WithSignlePaneBorderText(
17− map[layout.BorderPosition]string{
18− layout.TopMiddleBorder: "Logs",
19− },
16+ },
20− ),
17+ layout.WithBentoLayoutCurrentPane(layout.BentoRightTopPane),
21− layout.WithSinglePanePadding(1),
18+ layout.WithBentoLayoutRightTopHeightRatio(0.5),
22 )
19 )
23− p.Focus()
24− return p
25}
20}
internal/tui/tui.go+62−22
@@ -5,6 +5,7 @@ import (
5 tea "github.com/charmbracelet/bubbletea"
5 tea "github.com/charmbracelet/bubbletea"
6 "github.com/charmbracelet/lipgloss"
6 "github.com/charmbracelet/lipgloss"
7 "github.com/kujtimiihoxha/termai/internal/app"
7 "github.com/kujtimiihoxha/termai/internal/app"
8+ "github.com/kujtimiihoxha/termai/internal/logging"
8 "github.com/kujtimiihoxha/termai/internal/permission"
9 "github.com/kujtimiihoxha/termai/internal/permission"
9 "github.com/kujtimiihoxha/termai/internal/pubsub"
10 "github.com/kujtimiihoxha/termai/internal/pubsub"
10 "github.com/kujtimiihoxha/termai/internal/tui/components/core"
11 "github.com/kujtimiihoxha/termai/internal/tui/components/core"
@@ -74,22 +75,9 @@ func (a appModel) Init() tea.Cmd {
74}
75}
75
76
76func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
77func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
78+ var cmds []tea.Cmd
79+ var cmd tea.Cmd
77 switch msg := msg.(type) {
80 switch msg := msg.(type) {
78− case pubsub.Event[permission.PermissionRequest]:
79− return a, dialog.NewPermissionDialogCmd(msg.Payload)
80− case pubsub.Event[util.InfoMsg]:
81− a.status, _ = a.status.Update(msg)
82− case dialog.PermissionResponseMsg:
83− switch msg.Action {
84− case dialog.PermissionAllow:
85− a.app.Permissions.Grant(msg.Permission)
86− case dialog.PermissionAllowForSession:
87− a.app.Permissions.GrantPersistant(msg.Permission)
88− case dialog.PermissionDeny:
89− a.app.Permissions.Deny(msg.Permission)
90− }
91− case vimtea.EditorModeMsg:
92− a.editorMode = msg.Mode
93 case tea.WindowSizeMsg:
81 case tea.WindowSizeMsg:
94 var cmds []tea.Cmd
82 var cmds []tea.Cmd
95 msg.Height -= 1 // Make space for the status bar
83 msg.Height -= 1 // Make space for the status bar
@@ -109,6 +97,58 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
109 a.dialog = d.(core.DialogCmp)
97 a.dialog = d.(core.DialogCmp)
110
98
111 return a, tea.Batch(cmds...)
99 return a, tea.Batch(cmds...)
100+
101+ // Status
102+ case util.InfoMsg:
103+ a.status, cmd = a.status.Update(msg)
104+ return a, cmd
105+ case pubsub.Event[logging.LogMessage]:
106+ if msg.Payload.Persist {
107+ switch msg.Payload.Level {
108+ case "error":
109+ a.status, cmd = a.status.Update(util.InfoMsg{
110+ Type: util.InfoTypeError,
111+ Msg: msg.Payload.Message,
112+ TTL: msg.Payload.PersistTime,
113+ })
114+ case "info":
115+ a.status, cmd = a.status.Update(util.InfoMsg{
116+ Type: util.InfoTypeInfo,
117+ Msg: msg.Payload.Message,
118+ TTL: msg.Payload.PersistTime,
119+ })
120+ case "warn":
121+ a.status, cmd = a.status.Update(util.InfoMsg{
122+ Type: util.InfoTypeWarn,
123+ Msg: msg.Payload.Message,
124+ TTL: msg.Payload.PersistTime,
125+ })
126+
127+ default:
128+ a.status, cmd = a.status.Update(util.InfoMsg{
129+ Type: util.InfoTypeInfo,
130+ Msg: msg.Payload.Message,
131+ TTL: msg.Payload.PersistTime,
132+ })
133+ }
134+ }
135+ case util.ClearStatusMsg:
136+ a.status, _ = a.status.Update(msg)
137+
138+ // Permission
139+ case pubsub.Event[permission.PermissionRequest]:
140+ return a, dialog.NewPermissionDialogCmd(msg.Payload)
141+ case dialog.PermissionResponseMsg:
142+ switch msg.Action {
143+ case dialog.PermissionAllow:
144+ a.app.Permissions.Grant(msg.Permission)
145+ case dialog.PermissionAllowForSession:
146+ a.app.Permissions.GrantPersistant(msg.Permission)
147+ case dialog.PermissionDeny:
148+ a.app.Permissions.Deny(msg.Permission)
149+ }
150+
151+ // Dialog
112 case core.DialogMsg:
152 case core.DialogMsg:
113 d, cmd := a.dialog.Update(msg)
153 d, cmd := a.dialog.Update(msg)
114 a.dialog = d.(core.DialogCmp)
154 a.dialog = d.(core.DialogCmp)
@@ -119,10 +159,13 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
119 a.dialog = d.(core.DialogCmp)
159 a.dialog = d.(core.DialogCmp)
120 a.dialogVisible = false
160 a.dialogVisible = false
121 return a, cmd
161 return a, cmd
162+
163+ // Editor
164+ case vimtea.EditorModeMsg:
165+ a.editorMode = msg.Mode
166+
122 case page.PageChangeMsg:
167 case page.PageChangeMsg:
123 return a, a.moveToPage(msg.ID)
168 return a, a.moveToPage(msg.ID)
124− case util.InfoMsg:
125− a.status, _ = a.status.Update(msg)
126 case tea.KeyMsg:
169 case tea.KeyMsg:
127 if a.editorMode == vimtea.ModeNormal {
170 if a.editorMode == vimtea.ModeNormal {
128 switch {
171 switch {
@@ -162,9 +205,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
162 }
205 }
163 }
206 }
164
207
165− var cmds []tea.Cmd
166− s, cmd := a.status.Update(msg)
208+ a.status, cmd = a.status.Update(msg)
167− a.status = s
168 cmds = append(cmds, cmd)
209 cmds = append(cmds, cmd)
169 if a.dialogVisible {
210 if a.dialogVisible {
170 d, cmd := a.dialog.Update(msg)
211 d, cmd := a.dialog.Update(msg)
@@ -172,8 +213,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
172 cmds = append(cmds, cmd)
213 cmds = append(cmds, cmd)
173 return a, tea.Batch(cmds...)
214 return a, tea.Batch(cmds...)
174 }
215 }
175− p, cmd := a.pages[a.currentPage].Update(msg)
216+ a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
176− a.pages[a.currentPage] = p
177 cmds = append(cmds, cmd)
217 cmds = append(cmds, cmd)
178 return a, tea.Batch(cmds...)
218 return a, tea.Batch(cmds...)
179}
219}
internal/tui/util/util.go+6−1
@@ -1,6 +1,10 @@
1package util
1package util
2
2
3+import (
4+ "time"
5+
3−import tea "github.com/charmbracelet/bubbletea"
6+ tea "github.com/charmbracelet/bubbletea"
7+)
4
8
5func CmdHandler(msg tea.Msg) tea.Cmd {
9func CmdHandler(msg tea.Msg) tea.Cmd {
6 return func() tea.Msg {
10 return func() tea.Msg {
@@ -41,6 +45,7 @@ type (
41 InfoMsg struct {
45 InfoMsg struct {
42 Type InfoType
46 Type InfoType
43 Msg string
47 Msg string
48+ TTL time.Duration
44 }
49 }
45 ClearStatusMsg struct{}
50 ClearStatusMsg struct{}
46)
51)
main.go−12
@@ -1,21 +1,9 @@
1package main
1package main
2
2
3import (
3import (
4− "log"
5− "os"
6−
7 "github.com/kujtimiihoxha/termai/cmd"
4 "github.com/kujtimiihoxha/termai/cmd"
8)
5)
9
6
10func main() {
7func main() {
11− // Create a log file and make that the log output DEBUG
12− // TODO: remove this on release
13− logfile, err := os.OpenFile("debug.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
14− if err != nil {
15− panic(err)
16− }
17−
18− log.SetOutput(logfile)
19−
20 cmd.Execute()
8 cmd.Execute()
21}
9}