search and/or read your saved and liked bluesky posts
wails go svelte sqlite desktop bluesky
at main 271 lines 5.8 kB view raw
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}