Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: build test run clean lint fmt vet build-full generate_screenshots
.PHONY: build test run run-log clean lint fmt vet build-full generate_screenshots

BINARY_NAME=matcha
BUILD_DIR=bin
Expand Down Expand Up @@ -36,6 +36,9 @@ build-full:
run:
go run .

run-log:
go run . --debug --logs

test:
go test ./...

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/ProtonMail/go-crypto v1.4.1
github.com/PuerkitoBio/goquery v1.12.0
github.com/arran4/golang-ical v0.3.5
github.com/charmbracelet/x/ansi v0.11.7
github.com/ebfe/scard v0.0.0-20241214075232-7af069cabc25
github.com/emersion/go-imap/v2 v2.0.0-beta.8
github.com/emersion/go-maildir v0.6.0
Expand All @@ -38,7 +39,6 @@ require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 // indirect
github.com/charmbracelet/x/ansi v0.11.7 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
Expand Down
4 changes: 3 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"previous": "Previous",
"loading": "Loading...",
"error": "Error",
"success": "Success"
"success": "Success",
"logs": "Logs",
"no_logs_yet": "No logs yet"
},
"composer": {
"title": "Compose New Email",
Expand Down
79 changes: 79 additions & 0 deletions internal/logging/buffer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package logging

import (
"strings"
"sync"
)

type Buffer struct {
mu sync.Mutex
maxEntries int
entries []Entry
subs []chan Entry
}

func NewBuffer(maxEntries int) *Buffer {
if maxEntries < 1 {
maxEntries = DefaultMaxEntries
}
return &Buffer{maxEntries: maxEntries}
}

func (b *Buffer) MaxEntries() int {
return b.maxEntries
}

func (b *Buffer) Write(p []byte) (int, error) {
for _, line := range strings.Split(strings.TrimRight(string(p), "\n"), "\n") {
if line == "" {
continue
}
b.append(Entry{Text: line})
}
return len(p), nil
}

func (b *Buffer) Subscribe() <-chan Entry {
ch := make(chan Entry, 64)
b.mu.Lock()
b.subs = append(b.subs, ch)
b.mu.Unlock()
return ch
}

func (b *Buffer) Tail(n int) []Entry {
b.mu.Lock()
defer b.mu.Unlock()

if n <= 0 || len(b.entries) == 0 {
return nil
}
if n > len(b.entries) {
n = len(b.entries)
}

start := len(b.entries) - n
entries := make([]Entry, n)
copy(entries, b.entries[start:])
return entries
}

func (b *Buffer) append(entry Entry) {
b.mu.Lock()
if len(b.entries) >= b.maxEntries {
copy(b.entries, b.entries[1:])
b.entries[len(b.entries)-1] = entry
} else {
b.entries = append(b.entries, entry)
}

subs := append([]chan Entry(nil), b.subs...)
b.mu.Unlock()

for _, ch := range subs {
select {
case ch <- entry:
default:
}
}
}
73 changes: 73 additions & 0 deletions internal/logging/buffer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package logging

import "testing"

func TestBufferStoresLines(t *testing.T) {
buffer := NewBuffer(DefaultMaxEntries)

if _, err := buffer.Write([]byte("first\nsecond\n")); err != nil {
t.Fatalf("Write returned error: %v", err)
}

got := buffer.Tail(DefaultMaxEntries)
if len(got) != 2 {
t.Fatalf("Tail returned %d entries, want 2", len(got))
}
if got[0].Text != "first" || got[1].Text != "second" {
t.Fatalf("unexpected entries: %+v", got)
}
}

func TestBufferKeepsLastMaxEntries(t *testing.T) {
buffer := NewBuffer(DefaultMaxEntries)

for i := 0; i < DefaultMaxEntries+2; i++ {
if _, err := buffer.Write([]byte{byte('a' + i), '\n'}); err != nil {
t.Fatalf("Write returned error: %v", err)
}
}

got := buffer.Tail(DefaultMaxEntries)
if len(got) != DefaultMaxEntries {
t.Fatalf("Tail returned %d entries, want %d", len(got), DefaultMaxEntries)
}
if got[0].Text != "c" {
t.Fatalf("first retained entry = %q, want %q", got[0].Text, "c")
}
}

func TestBufferTailReturnsRequestedCount(t *testing.T) {
buffer := NewBuffer(DefaultMaxEntries)

for _, line := range []string{"first\n", "second\n", "third\n"} {
if _, err := buffer.Write([]byte(line)); err != nil {
t.Fatalf("Write returned error: %v", err)
}
}

got := buffer.Tail(2)
if len(got) != 2 {
t.Fatalf("Tail returned %d entries, want 2", len(got))
}
if got[0].Text != "second" || got[1].Text != "third" {
t.Fatalf("unexpected entries: %+v", got)
}
}

func TestBufferTailReturnsNilForNonPositiveCount(t *testing.T) {
buffer := NewBuffer(DefaultMaxEntries)

if _, err := buffer.Write([]byte("first\n")); err != nil {
t.Fatalf("Write returned error: %v", err)
}
if got := buffer.Tail(0); got != nil {
t.Fatalf("Tail(0) returned %+v, want nil", got)
}
}

func TestNewBufferUsesDefaultForInvalidMax(t *testing.T) {
buffer := NewBuffer(0)
if got := buffer.MaxEntries(); got != DefaultMaxEntries {
t.Fatalf("MaxEntries = %d, want %d", got, DefaultMaxEntries)
}
}
16 changes: 16 additions & 0 deletions internal/logging/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package logging

import "io"

const DefaultMaxEntries = 10

type Entry struct {
Text string
}

type Logger interface {
io.Writer
MaxEntries() int
Tail(n int) []Entry
Subscribe() <-chan Entry
}
4 changes: 2 additions & 2 deletions internal/loglevel/level.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ func Debugf(format string, args ...any) {

func Verbosef(format string, args ...any) {
if Get() >= LevelVerbose {
log.Printf(format, args...)
log.Printf("verbose: "+format, args...)
}
}

func Infof(format string, args ...any) {
if Get() >= LevelInfo {
log.Printf(format, args...)
log.Printf("info: "+format, args...)
}
}
4 changes: 2 additions & 2 deletions internal/loglevel/level_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestVerbosefRequiresVerboseLevel(t *testing.T) {
output := captureLog(t, func() {
Verbosef("more details")
})
if !strings.Contains(output, "more details") {
if !strings.Contains(output, "verbose: more details") {
t.Fatalf("Verbosef did not write at level %v: %q", level, output)
}
}
Expand All @@ -79,7 +79,7 @@ func TestInfofRequiresInfoLevel(t *testing.T) {
output = captureLog(t, func() {
Infof("hello")
})
if !strings.Contains(output, "hello") {
if !strings.Contains(output, "info: hello") {
t.Fatalf("Infof did not write at info level: %q", output)
}
}
Expand Down
Loading
Loading