feat(release): v0.1.0

commit 06ed2c3cbe
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 11:34:24 2026 +0100

    fix: changed detected by scanner but no updated by render layer

commit 01dcaf882a
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 10:19:05 2026 +0100

    feat: VERSION bumb

commit 229223f77a
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 09:53:08 2026 +0100

    feat: filter and search by tag

commit cb11e34798
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 09:41:03 2026 +0100

    feat: tag system

commit 3f5cf0d673
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 09:15:29 2026 +0100

    feat: sqlite storage draft

commit d6617cec02
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 09:04:11 2026 +0100

    feat: metadata draft

commit 7238d02a13
Author: adminoo <git@kadath.corp>
Date:   Mon Feb 2 10:18:42 2026 +0100

    fix: body overflowing

commit 16ff836274
Author: adminoo <git@kadath.corp>
Date:   Mon Feb 2 10:09:01 2026 +0100

    feat: tests for http handlers and render package

commit 36ac3f03aa
Author: adminoo <git@kadath.corp>
Date:   Mon Feb 2 09:45:29 2026 +0100

    feat: Dark theme, placeholder metadata panel

commit e6923fa4f5
Author: adminoo <git@kadath.corp>
Date:   Sun Feb 1 18:26:59 2026 +0100

    fix: uneeded func + uneeded bogus note creation logic

commit 4458ba2d15
Author: adminoo <git@kadath.corp>
Date:   Sun Feb 1 18:26:21 2026 +0100

    feat: log when changing note states

commit 92a6f84540
Author: adminoo <git@kadath.corp>
Date:   Sun Feb 1 16:55:40 2026 +0100

    possibly first working draft

commit e27aadc603
Author: adminoo <git@kadath.corp>
Date:   Sun Feb 1 11:55:16 2026 +0100

    draft shits
This commit is contained in:
2026-02-03 12:01:17 +01:00
parent d17ed8c650
commit 9d1254244f
27 changed files with 2940 additions and 0 deletions

View File

@ -0,0 +1,70 @@
package scanner
import (
"path/filepath"
"donniemarko/internal/note"
"donniemarko/internal/storage"
"log"
"os"
)
type NotesHandler struct {
storage storage.Storage
}
func NewNotesHandler(storage storage.Storage) *NotesHandler {
return &NotesHandler{storage: storage}
}
func (h *NotesHandler) HandleCreate(path string) error {
note, err := ParseNoteFile(path)
if err != nil {
return err
}
if err := h.storage.Create(note); err != nil {
return err
}
log.Printf("Created or updated note '%s'\n", path)
return nil
}
func (h *NotesHandler) HandleModify(path string) error {
return h.HandleCreate(path)
}
func (h *NotesHandler) HandleDelete(path string) error {
id := note.GenerateNoteID(path)
h.storage.Delete(id)
log.Printf("Deleted note '%s' from index\n", path)
return nil
}
// ParseNoteFile reads a note file on the file system and returns a note struct
// populated with the metadata and content extracted from the file
func ParseNoteFile(path string) (*note.Note, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// Get file info to get modification time
fileInfo, err := os.Stat(path)
if err != nil {
return nil, err
}
id := note.GenerateNoteID(path)
nn := note.NewNote()
nn.ID = id
nn.Path = path
nn.Content = string(content)
nn.Title = note.ExtractTitle(nn.Content)
// Use filename as title if no heading found
if nn.Title == "" {
nn.Title = filepath.Base(path)
}
nn.UpdatedAt = fileInfo.ModTime()
nn.Size = fileInfo.Size()
return nn, nil
}

168
internal/scanner/scanner.go Normal file
View File

@ -0,0 +1,168 @@
package scanner
import (
"context"
"log"
"os"
"path/filepath"
"strings"
"time"
)
type ChangeType int
const (
Unchanged ChangeType = iota
Created
Modified
Deleted
)
type ChangeHandler interface {
HandleCreate(path string) error
HandleModify(path string) error
HandleDelete(path string) error
}
type Change struct {
Type ChangeType
Path string
ModTime time.Time
}
type ScannerService struct {
RootDir string
Interval time.Duration
LastStates map[string]time.Time
handler ChangeHandler
}
func NewScanner(path string) *ScannerService {
return &ScannerService{
RootDir: path,
Interval: 5 * time.Second,
LastStates: make(map[string]time.Time),
}
}
func (s *ScannerService) SetHandler(handler ChangeHandler) {
s.handler = handler
}
// Scan walks the root folder and update the states of each notes if
// it has changed since the last time a scan occured
func (s *ScannerService) Scan() ([]Change, error) {
var changes []Change
currentStates := make(map[string]time.Time)
// Walk filesystem
filepath.Walk(s.RootDir, func(path string, info os.FileInfo, err error) error {
// skip the root dir itself
if s.RootDir == path {
return nil
}
// ignore anything that isn't a note
if !isValidNoteFile(path, info) {
return nil
}
currentStates[path] = info.ModTime()
lastMod, existed := s.LastStates[path]
if !existed {
// create the note if it didn't exist yet
// s.handler.HandleCreate(path)
changes = append(changes, Change{Type: Created, Path: path, ModTime: lastMod})
} else if info.ModTime().After(lastMod) {
changes = append(changes, Change{Type: Modified, Path: path, ModTime: info.ModTime()})
}
return nil
})
// Check for deletions
for path := range s.LastStates {
if _, exists := currentStates[path]; !exists {
changes = append(changes, Change{Type: Deleted, Path: path})
}
}
s.LastStates = currentStates
return changes, nil
}
// Monitor rescan the root folder at each new tick and handle state modifications
func (s *ScannerService) Monitor(ctx context.Context) error {
ticker := time.NewTicker(s.Interval)
defer ticker.Stop()
applyChanges := func(changes []Change) {
for _, change := range changes {
var err error
switch change.Type {
case Created:
err = s.handler.HandleCreate(change.Path)
case Modified:
err = s.handler.HandleModify(change.Path)
case Deleted:
err = s.handler.HandleDelete(change.Path)
}
if err != nil {
log.Printf("handler error for %s: %v", change.Path, err)
}
}
}
changes, err := s.Scan()
if err != nil {
log.Printf("scan error: %v", err)
} else {
applyChanges(changes)
}
for {
select {
case <-ticker.C:
changes, err := s.Scan()
if err != nil {
log.Printf("scan error: %v", err)
continue
}
applyChanges(changes)
case <-ctx.Done():
return ctx.Err()
}
}
}
func isValidNoteFile(path string, info os.FileInfo) bool {
// ignore temp and backup files
for _, nono := range []string{".#", "~"} {
if strings.Contains(path, nono) {
return false
}
}
// ignore files that are not markdown
if info.IsDir() || filepath.Ext(path) != ".md" {
return false
}
// ignore empty folder
if info.IsDir() {
files, err := os.ReadDir(path)
if err != nil {
log.Println(err.Error())
}
if len(files) == 0 {
return false
}
}
return true
}

View File

@ -0,0 +1,58 @@
package scanner
import (
"os"
"path/filepath"
"testing"
"time"
)
func TestScanner_DetectsNewFile(t *testing.T) {
tmpDir := t.TempDir()
scanner := NewScanner(tmpDir)
scanner.Scan() // Initial scan
os.WriteFile(filepath.Join(tmpDir, "new.md"), []byte("# New"), 0644)
changes, _ := scanner.Scan()
if len(changes) != 1 || changes[0].Type != Created {
t.Error("should detect new file")
}
}
func TestScanner_DetectChanges(t *testing.T) {
tmpDir := t.TempDir()
filePath := filepath.Join(tmpDir, "test.md")
// Initial state
os.WriteFile(filePath, []byte("# Original"), 0644)
scanner := NewScanner(tmpDir)
changes, _ := scanner.Scan()
originalModTime := changes[0].ModTime
// Wait and modify
time.Sleep(10 * time.Millisecond)
os.WriteFile(filePath, []byte("# Modified"), 0644)
changes, _ = scanner.Scan()
newModTime := changes[0].ModTime
if !newModTime.After(originalModTime) {
t.Error("should detect file modification")
}
if changes[0].Type != Modified {
t.Errorf("Last state should be modified, got '%v'\n", changes[0].Type)
}
newPath := filepath.Join(tmpDir, "test_renamed.md")
os.Rename(filePath, newPath)
changes, _ = scanner.Scan()
if changes[0].Path != newPath {
t.Errorf("Should find renamed file '%s'. Got '%s'\n", newPath, changes[0].Path)
}
}