search and/or read your saved and liked bluesky posts
wails
go
svelte
sqlite
desktop
bluesky
1package main
2
3import (
4 "bufio"
5 "context"
6 "fmt"
7 "io"
8 "os"
9 "path/filepath"
10 "strings"
11 "sync"
12 "time"
13
14 "github.com/wailsapp/wails/v2/pkg/runtime"
15)
16
17// LogLevel represents the severity of a log message
18type LogLevel string
19
20const (
21 DebugLevel LogLevel = "DEBUG"
22 InfoLevel LogLevel = "INFO"
23 WarnLevel LogLevel = "WARN"
24 ErrorLevel LogLevel = "ERROR"
25)
26
27// LogEntry represents a single log entry
28type LogEntry struct {
29 Level string `json:"level"`
30 Message string `json:"message"`
31 Timestamp time.Time `json:"timestamp"`
32}
33
34// LogService provides logging functionality via Wails bindings
35type LogService struct {
36 ctx context.Context
37 mu sync.RWMutex
38 entries []LogEntry
39 maxEntries int
40 file *os.File
41 writer *LogWriter
42 level LogLevel
43}
44
45// LogWriter implements io.Writer and emits Wails events
46type LogWriter struct {
47 service *LogService
48 mu sync.Mutex
49}
50
51func (w *LogWriter) Write(p []byte) (n int, err error) {
52 w.mu.Lock()
53 defer w.mu.Unlock()
54
55 lines := strings.Split(string(p), "\n")
56 for _, line := range lines {
57 line = strings.TrimSpace(line)
58 if line == "" {
59 continue
60 }
61
62 level := w.parseLevel(line)
63
64 entry := LogEntry{
65 Level: string(level),
66 Message: line,
67 Timestamp: time.Now(),
68 }
69
70 w.service.addEntry(entry)
71 w.service.emitLogLine(entry)
72 }
73
74 return len(p), nil
75}
76
77func (w *LogWriter) parseLevel(line string) LogLevel {
78 upper := strings.ToUpper(line)
79 if strings.Contains(upper, "ERROR") || strings.Contains(upper, "ERR") {
80 return ErrorLevel
81 }
82 if strings.Contains(upper, "WARN") || strings.Contains(upper, "WARNING") {
83 return WarnLevel
84 }
85 if strings.Contains(upper, "DEBUG") || strings.Contains(upper, "DBG") {
86 return DebugLevel
87 }
88 return InfoLevel
89}
90
91// NewLogService creates a new LogService instance
92func NewLogService() *LogService {
93 return &LogService{
94 entries: make([]LogEntry, 0),
95 maxEntries: 1000,
96 level: InfoLevel,
97 }
98}
99
100func (s *LogService) setContext(ctx context.Context) {
101 s.ctx = ctx
102}
103
104// Initialize sets up the log service with a file writer
105func (s *LogService) Initialize() error {
106 logPath := s.getLogPath()
107 logDir := filepath.Dir(logPath)
108 if err := os.MkdirAll(logDir, 0755); err != nil {
109 return fmt.Errorf("failed to create log directory: %w", err)
110 }
111
112 file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
113 if err != nil {
114 return fmt.Errorf("failed to open log file: %w", err)
115 }
116 s.file = file
117
118 s.writer = &LogWriter{service: s}
119 return nil
120}
121
122// Close closes the log file
123func (s *LogService) Close() error {
124 if s.file != nil {
125 return s.file.Close()
126 }
127 return nil
128}
129
130// GetWriter returns the io.Writer for logging
131func (s *LogService) GetWriter() io.Writer {
132 if s.writer == nil {
133 return io.Discard
134 }
135 return s.writer
136}
137
138// GetMultiWriter returns a MultiWriter that writes to both the log file and the event emitter
139func (s *LogService) GetMultiWriter() io.Writer {
140 if s.file == nil || s.writer == nil {
141 return io.Discard
142 }
143 return io.MultiWriter(s.file, s.writer)
144}
145
146// GetEntries returns all log entries
147func (s *LogService) GetEntries() []LogEntry {
148 s.mu.RLock()
149 defer s.mu.RUnlock()
150
151 result := make([]LogEntry, len(s.entries))
152 copy(result, s.entries)
153 return result
154}
155
156// GetEntriesByLevel returns log entries filtered by level
157func (s *LogService) GetEntriesByLevel(level string) []LogEntry {
158 s.mu.RLock()
159 defer s.mu.RUnlock()
160
161 var result []LogEntry
162 for _, entry := range s.entries {
163 if entry.Level == level {
164 result = append(result, entry)
165 }
166 }
167 return result
168}
169
170// Clear clears all log entries
171func (s *LogService) Clear() {
172 s.mu.Lock()
173 defer s.mu.Unlock()
174
175 s.entries = make([]LogEntry, 0)
176 s.emitLogCleared()
177}
178
179// SetLevel sets the minimum log level
180func (s *LogService) SetLevel(level string) {
181 s.mu.Lock()
182 defer s.mu.Unlock()
183
184 s.level = LogLevel(level)
185}
186
187// GetLevel returns the current log level
188func (s *LogService) GetLevel() string {
189 s.mu.RLock()
190 defer s.mu.RUnlock()
191
192 return string(s.level)
193}
194
195func (s *LogService) addEntry(entry LogEntry) {
196 s.mu.Lock()
197 defer s.mu.Unlock()
198
199 if !s.shouldLog(entry.Level) {
200 return
201 }
202
203 s.entries = append(s.entries, entry)
204
205 if len(s.entries) > s.maxEntries {
206 s.entries = s.entries[len(s.entries)-s.maxEntries:]
207 }
208}
209
210func (s *LogService) shouldLog(entryLevel string) bool {
211 levels := map[LogLevel]int{
212 DebugLevel: 0,
213 InfoLevel: 1,
214 WarnLevel: 2,
215 ErrorLevel: 3,
216 }
217
218 entryIdx := levels[LogLevel(entryLevel)]
219 currentIdx := levels[s.level]
220
221 return entryIdx >= currentIdx
222}
223
224func (s *LogService) emitLogLine(entry LogEntry) {
225 if s.ctx != nil {
226 runtime.EventsEmit(s.ctx, "log:line", entry)
227 }
228}
229
230func (s *LogService) emitLogCleared() {
231 if s.ctx != nil {
232 runtime.EventsEmit(s.ctx, "log:cleared", map[string]any{})
233 }
234}
235
236func (s *LogService) getLogPath() string {
237 if path := os.Getenv("BSKY_BROWSER_LOG"); path != "" {
238 return path
239 }
240
241 configDir := os.Getenv("XDG_CONFIG_HOME")
242 if configDir == "" {
243 home, _ := os.UserHomeDir()
244 configDir = filepath.Join(home, ".config")
245 }
246
247 appDir := filepath.Join(configDir, "bsky-browser", "logs")
248 timestamp := time.Now().Format("2006-01-02_15-04-05")
249 return filepath.Join(appDir, fmt.Sprintf("bsky-browser_%s.log", timestamp))
250}
251
252// BufferedLogWriter wraps a writer with buffering for better performance
253type BufferedLogWriter struct {
254 writer *bufio.Writer
255 service *LogService
256}
257
258func NewBufferedLogWriter(service *LogService) *BufferedLogWriter {
259 return &BufferedLogWriter{
260 writer: bufio.NewWriter(service.GetMultiWriter()),
261 service: service,
262 }
263}
264
265func (w *BufferedLogWriter) Write(p []byte) (n int, err error) {
266 return w.writer.Write(p)
267}
268
269func (w *BufferedLogWriter) Flush() error {
270 return w.writer.Flush()
271}