Files
libretechandClaude Opus 4.7 df9407dd8d Fork Notesium source and restructure into Go package layout
Initial fork of github.com/alonswartz/notesium into librenotes:
- Source moved to internal/notesium/ (package notesium)
- Thin entry point at cmd/librenotes/main.go
- Module renamed to git.librete.ch/public/librenotes
- main() exposed as notesium.Run()
- LICENSE preserved (MIT), NOTICE added with attribution
- Web assets and completion.bash co-located with embedding code
  to satisfy go:embed path constraints

Closes #3, #34, #35.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 21:52:25 +02:00

606 lines
15 KiB
Go

package notesium
import (
"bufio"
"embed"
"fmt"
"io"
"io/fs"
"log"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
)
var buildtime = "unset"
var gitversion = "unset"
//go:embed completion.bash web/app
var embedfs embed.FS
var embedfsWebRoot = "web/app"
func Run() {
cmd, err := parseOptions(os.Args[1:])
if err != nil {
log.Fatal(err)
}
switch cmd.Name {
case "help":
fmt.Print(usage)
os.Exit(1)
case "version":
notesiumVersion(cmd.Options.(versionOptions), os.Stdout)
return
}
notesiumDir, err := getNotesiumDir()
if err != nil {
log.Fatal(err)
}
switch cmd.Name {
case "home":
fmt.Println(notesiumDir)
case "new":
notesiumNew(notesiumDir, cmd.Options.(newOptions), os.Stdout)
case "list":
notesiumList(notesiumDir, cmd.Options.(listOptions), os.Stdout)
case "links":
notesiumLinks(notesiumDir, cmd.Options.(linksOptions), os.Stdout)
case "lines":
notesiumLines(notesiumDir, cmd.Options.(linesOptions), os.Stdout)
case "stats":
notesiumStats(notesiumDir, cmd.Options.(statsOptions), os.Stdout)
case "finder":
notesiumFinder(notesiumDir, cmd.Options.(finderOptions))
case "cat":
notesiumCat(notesiumDir, cmd.Options.(catOptions))
case "web":
notesiumWeb(notesiumDir, cmd.Options.(webOptions))
case "extract":
notesiumExtract(cmd.Options.(extractOptions))
}
}
func notesiumNew(dir string, opts newOptions, w io.Writer) {
ctime := time.Now()
if opts.ctime != "" {
var err error
ctime, err = time.ParseInLocation("2006-01-02T15:04:05", opts.ctime, time.Local)
if err != nil {
log.Fatalf("invalid ctime format: %v", err)
}
}
epochInt := ctime.Unix()
epochHex := fmt.Sprintf("%x", epochInt)
filename := fmt.Sprintf("%s.md", epochHex)
path := filepath.Join(dir, filename)
_, err := os.Stat(path)
fileExists := !os.IsNotExist(err)
if opts.verbose {
fmt.Fprintf(w, "path:%s\n", path)
fmt.Fprintf(w, "filename:%s\n", filename)
fmt.Fprintf(w, "epoch:%d\n", epochInt)
fmt.Fprintf(w, "ctime:%s\n", ctime.Format("2006-01-02T15:04:05-07:00"))
fmt.Fprintf(w, "exists:%t\n", fileExists)
} else {
fmt.Fprintf(w, "%s\n", path)
}
}
func notesiumList(dir string, opts listOptions, w io.Writer) {
populateCache(dir)
notes := getSortedNotes(opts.sortBy)
switch opts.limit {
case "labels":
for _, note := range notes {
if note.IsLabel {
fmt.Fprintf(w, "%s:1: %s\n", note.Filename, note.Title)
}
}
return
case "orphans":
for _, note := range notes {
if len(note.OutgoingLinks) == 0 && len(note.IncomingLinks) == 0 {
fmt.Fprintf(w, "%s:1: %s\n", note.Filename, note.Title)
}
}
return
}
switch opts.prefix {
case "label":
var notesWithoutLabelLinks []*Note
var outputLines []string
for _, note := range notes {
labelLinked := false
for _, link := range note.OutgoingLinks {
if linkNote, exists := noteCache[link.Filename]; exists && linkNote.IsLabel {
line := fmt.Sprintf("%s:1: %s%s%s %s", note.Filename, opts.color.Code, linkNote.Title, opts.color.Reset, note.Title)
if opts.sortBy == "alpha" {
outputLines = append(outputLines, line)
} else {
fmt.Fprintln(w, line)
}
labelLinked = true
}
}
if !labelLinked {
notesWithoutLabelLinks = append(notesWithoutLabelLinks, note)
}
}
if opts.sortBy == "alpha" {
sortLinesByField(outputLines, ": ", 1)
for _, line := range outputLines {
fmt.Fprintln(w, line)
}
}
for _, note := range notesWithoutLabelLinks {
fmt.Fprintf(w, "%s:1: %s\n", note.Filename, note.Title)
}
return
case "ctime":
for _, note := range notes {
dateStamp := getDateStamp(note.Ctime, opts.dateFormat)
fmt.Fprintf(w, "%s:1: %s%s%s %s\n", note.Filename, opts.color.Code, dateStamp, opts.color.Reset, note.Title)
}
return
case "mtime":
for _, note := range notes {
dateStamp := getDateStamp(note.Mtime, opts.dateFormat)
fmt.Fprintf(w, "%s:1: %s%s%s %s\n", note.Filename, opts.color.Code, dateStamp, opts.color.Reset, note.Title)
}
return
}
for _, note := range notes {
fmt.Fprintf(w, "%s:1: %s\n", note.Filename, note.Title)
}
}
func notesiumLinks(dir string, opts linksOptions, w io.Writer) {
populateCache(dir)
if opts.filename != "" {
note, exists := noteCache[opts.filename]
if !exists {
log.Printf("filename does not exist: %s\n", opts.filename)
return
}
switch opts.limit {
case "outgoing":
for _, link := range note.OutgoingLinks {
linkNote, exists := noteCache[link.Filename]
if exists {
fmt.Fprintf(w, "%s:1: %s\n", linkNote.Filename, linkNote.Title)
}
}
return
case "incoming":
for _, link := range note.IncomingLinks {
linkNote, exists := noteCache[link.Filename]
if exists {
fmt.Fprintf(w, "%s:%d: %s\n", linkNote.Filename, link.LineNumber, linkNote.Title)
}
}
return
default:
prefix := fmt.Sprintf("%soutgoing%s", opts.color.Code, opts.color.Reset)
for _, link := range note.OutgoingLinks {
linkNote, exists := noteCache[link.Filename]
if exists {
fmt.Fprintf(w, "%s:1: %s %s\n", linkNote.Filename, prefix, linkNote.Title)
}
}
prefix = fmt.Sprintf("%sincoming%s", opts.color.Code, opts.color.Reset)
for _, link := range note.IncomingLinks {
linkNote, exists := noteCache[link.Filename]
if exists {
fmt.Fprintf(w, "%s:%d: %s %s\n", linkNote.Filename, link.LineNumber, prefix, linkNote.Title)
}
}
}
return
}
switch opts.limit {
case "dangling":
for _, note := range noteCache {
for _, link := range note.OutgoingLinks {
_, exists := noteCache[link.Filename]
if !exists {
fmt.Fprintf(w, "%s:%d: %s%s%s → %s\n", note.Filename, link.LineNumber, opts.color.Code, note.Title, opts.color.Reset, link.Filename)
}
}
}
return
}
notes := getSortedNotes("alpha")
for _, note := range notes {
for _, link := range note.OutgoingLinks {
linkNote, exists := noteCache[link.Filename]
linkTitle := link.Filename
if exists {
linkTitle = linkNote.Title
}
fmt.Fprintf(w, "%s:%d: %s%s%s → %s\n", note.Filename, link.LineNumber, opts.color.Code, note.Title, opts.color.Reset, linkTitle)
}
}
}
func notesiumLines(dir string, opts linesOptions, w io.Writer) {
files, err := os.ReadDir(dir)
if err != nil {
log.Fatalf("Could not read directory: %s\n", err)
}
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(file.Name(), ".md") {
filename := file.Name()
path := filepath.Join(dir, filename)
file, err := os.Open(path)
if err != nil {
log.Fatalf("Could not open file: %s\n", err)
}
var title string
scanner := bufio.NewScanner(file)
lineNumber := 0
for scanner.Scan() {
lineNumber++
line := scanner.Text()
if line == "" {
continue
}
if opts.prefix == "title" && title == "" && strings.HasPrefix(line, "# ") {
title = strings.TrimPrefix(line, "# ")
}
if opts.filter != "" {
matches, err := evaluateFilterQuery(opts.filter, line)
if err != nil {
fmt.Fprintf(w, "Error: %v\n", err)
continue
}
if !matches {
continue
}
}
if opts.prefix == "title" {
fmt.Fprintf(w, "%s:%d: %s%s%s %s\n", filename, lineNumber, opts.color.Code, title, opts.color.Reset, line)
} else {
fmt.Fprintf(w, "%s:%d: %s\n", filename, lineNumber, line)
}
}
if err := scanner.Err(); err != nil {
log.Fatalf("scanner error: %s", err)
}
file.Close()
}
}
}
func notesiumStats(dir string, opts statsOptions, w io.Writer) {
populateCache(dir)
labels := 0
orphans := 0
links := 0
dangling := 0
lines := 0
words := 0
chars := 0
for _, note := range noteCache {
if note.IsLabel {
labels++
}
if len(note.OutgoingLinks) == 0 && len(note.IncomingLinks) == 0 {
orphans++
}
for _, link := range note.OutgoingLinks {
_, exists := noteCache[link.Filename]
if !exists {
dangling++
}
}
links += len(note.OutgoingLinks)
lines += note.Lines
words += note.Words
chars += note.Chars
}
keyFormat := opts.color.Code + (map[bool]string{true: "%-9s", false: "%s"}[opts.table]) + opts.color.Reset
fmt.Fprintf(w, keyFormat+" %d\n", "notes", len(noteCache))
fmt.Fprintf(w, keyFormat+" %d\n", "labels", labels)
fmt.Fprintf(w, keyFormat+" %d\n", "orphans", orphans)
fmt.Fprintf(w, keyFormat+" %d\n", "links", links)
fmt.Fprintf(w, keyFormat+" %d\n", "dangling", dangling)
fmt.Fprintf(w, keyFormat+" %d\n", "lines", lines)
fmt.Fprintf(w, keyFormat+" %d\n", "words", words)
fmt.Fprintf(w, keyFormat+" %d\n", "chars", chars)
}
func notesiumFinder(dir string, opts finderOptions) {
inputCmd, err := parseOptions(opts.input)
if err != nil {
log.Fatal(err)
}
theme := os.Getenv("NOTESIUM_FINDER_THEME")
var fzfColor string
switch theme {
case "light":
fzfColor = "bg:15,bg+:7,fg:241,fg+:241,hl:4,hl+:2,pointer:9,info:6"
default:
fzfColor = "bg:8,bg+:0,fg:12,fg+:12,hl:11,hl+:3,pointer:9,info:3"
}
optsFzf := []string{
"--ansi",
"--exact",
"--border",
"--reverse",
"--no-sort",
"--no-height",
"--no-scrollbar",
"--no-separator",
"--pointer=>",
"--delimiter=:",
"--with-nth=3..",
fmt.Sprintf("--color=%s", fzfColor),
fmt.Sprintf("--prompt=%s> ", opts.prompt),
}
if opts.preview {
executablePath, err := os.Executable()
if err != nil {
log.Fatalf("error resolving executable path: %v", err)
}
executablePath, err = filepath.EvalSymlinks(executablePath)
if err != nil {
log.Fatalf("error resolving symlinked executable path: %v", err)
}
optsFzf = append(optsFzf,
"--bind=ctrl-/:toggle-preview",
"--preview-window=+{2}-/2",
fmt.Sprintf("--preview=%s cat {}", executablePath),
)
}
inputChan := make(chan string)
go func() {
defer close(inputChan)
writer := &channelWriter{ch: inputChan}
switch inputCmd.Name {
case "list":
notesiumList(dir, inputCmd.Options.(listOptions), writer)
case "links":
notesiumLinks(dir, inputCmd.Options.(linksOptions), writer)
case "lines":
notesiumLines(dir, inputCmd.Options.(linesOptions), writer)
default:
log.Fatal("input command not supported: ", inputCmd.Name)
}
}()
results, code, err := runFinder(inputChan, optsFzf)
if code != 0 && code != 130 && err != nil {
fmt.Fprintf(os.Stderr, "Error running fzf: %v\n", err)
}
for _, line := range results {
fmt.Print(line)
}
os.Exit(code)
}
func notesiumCat(dir string, opts catOptions) {
path := filepath.Join(dir, opts.filename)
file, err := os.Open(path)
if err != nil {
log.Fatalf("Error opening file: %v\n", err)
}
defer file.Close()
renderMarkdown(file, os.Stdout, opts.lineNumber)
}
func notesiumWeb(dir string, opts webOptions) {
populateCache(dir)
var httpfs http.FileSystem
if opts.webroot == "embedded" {
subfs, err := fs.Sub(embedfs, embedfsWebRoot)
if err != nil {
log.Fatalf("embedded webroot sub error: %v", err)
}
httpfs = http.FS(subfs)
} else {
httpfs = http.Dir(opts.webroot)
}
ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", opts.host, opts.port))
if err != nil {
log.Fatalf("Failed to listen on a port: %v", err)
}
defer ln.Close()
url := "http://localhost:" + strings.Split(ln.Addr().String(), ":")[1]
server := &http.Server{
Addr: ln.Addr().String(),
}
http.Handle("/", heartbeatH(http.FileServer(httpfs)))
http.HandleFunc("/api/notes", heartbeatF(apiList))
http.HandleFunc("/api/notes/", heartbeatF(func(w http.ResponseWriter, r *http.Request) {
apiNote(dir, w, r, opts.readOnly)
}))
http.HandleFunc("/api/runtime", heartbeatF(func(w http.ResponseWriter, r *http.Request) {
apiRuntime(dir, w, r, opts)
}))
http.HandleFunc("/api/raw/", heartbeatF(func(w http.ResponseWriter, r *http.Request) {
apiRaw(dir, w, r)
}))
for uri, srcDir := range opts.mounts {
mountfs := http.Dir(srcDir)
http.Handle(uri, http.StripPrefix(uri, heartbeatH(http.FileServer(mountfs))))
}
var idleStopMsg string
if opts.heartbeat {
idleStopMsg = " (stop-on-idle enabled)"
http.HandleFunc("/api/heartbeat", heartbeatF(apiHeartbeat))
go checkHeartbeat(server)
}
if opts.launchBrowser {
go func() {
time.Sleep(500 * time.Millisecond)
var cmd *exec.Cmd
switch runtime.GOOS {
case "linux":
cmd = exec.Command("xdg-open", url)
case "darwin":
cmd = exec.Command("open", url)
case "windows":
cmd = exec.Command("cmd", "/c", "start", url)
default:
log.Println("Unsupported OS for launching browser")
return
}
err := cmd.Start()
if err != nil {
log.Printf("Failed to launch the browser: %v", err)
}
}()
}
fmt.Printf("Serving on %s (bind address %s)\n", url, opts.host)
fmt.Printf("Press Ctrl+C to stop%s\n", idleStopMsg)
if err := server.Serve(ln); err != http.ErrServerClosed {
log.Fatalf("Server closed unexpected:%+v", err)
}
}
func notesiumExtract(opts extractOptions) {
switch opts.path {
case "":
var files []string
fs.WalkDir(embedfs, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
files = append(files, path)
}
return nil
})
for _, file := range files {
fmt.Println(file)
}
default:
content, err := fs.ReadFile(embedfs, opts.path)
if err != nil {
log.Fatalf("Failed to read file: %s", err)
}
fmt.Println(string(content))
}
}
func notesiumVersion(opts versionOptions, w io.Writer) {
version := getVersion(gitversion)
if opts.check {
latest, err := getLatestReleaseInfo()
if err != nil {
fmt.Fprintf(w, "Error getting latest release info: %v\n", err)
return
}
fmt.Fprintf(w, "Notesium %s (%s/%s)\n", version, runtime.GOOS, runtime.GOARCH)
comparison := compareVersions(version, latest.Version)
switch comparison {
case -1:
publishedAt := latest.PublishedAt
if parsedTime, err := time.Parse(time.RFC3339, latest.PublishedAt); err == nil {
publishedAt = parsedTime.Local().Format("2006-01-02 15:04")
}
fmt.Fprintf(w, "A new release is available: %s (%s)\n", latest.Version, publishedAt)
fmt.Fprintf(w, "https://github.com/alonswartz/notesium/releases\n")
case 0:
fmt.Fprintf(w, "You are using the latest version\n")
case 1:
fmt.Fprintf(w, "You are using a newer version than latest: %s\n", latest.Version)
}
if opts.verbose {
fmt.Fprintf(w, "\ncomparison:%d\n", comparison)
fmt.Fprintf(w, "version:%s\n", version)
fmt.Fprintf(w, "gitversion:%s\n", gitversion)
fmt.Fprintf(w, "buildtime:%s\n", buildtime)
fmt.Fprintf(w, "platform:%s/%s\n", runtime.GOOS, runtime.GOARCH)
fmt.Fprintf(w, "latest.version:%s\n", latest.Version)
fmt.Fprintf(w, "latest.published:%s\n", latest.PublishedAt)
fmt.Fprintf(w, "latest.release:%s\n", latest.HTMLURL)
}
return
}
if opts.verbose {
fmt.Fprintf(w, "version:%s\n", version)
fmt.Fprintf(w, "gitversion:%s\n", gitversion)
fmt.Fprintf(w, "buildtime:%s\n", buildtime)
fmt.Fprintf(w, "platform:%s/%s\n", runtime.GOOS, runtime.GOARCH)
} else {
fmt.Fprintf(w, "%s\n", version)
}
}
func getDateStamp(t time.Time, dateFormat string) string {
dateStamp := t.Format(dateFormat)
// experimental: monday first day of week
if strings.Contains(dateStamp, "%V") {
_, week := t.ISOWeek()
dateStamp = strings.Replace(dateStamp, "%V", fmt.Sprintf("%02d", week), 1)
}
// experimental: sunday first day of week
if strings.Contains(dateStamp, "%U") {
_, week := t.ISOWeek()
if t.Weekday() == time.Sunday {
_, week = t.AddDate(0, 0, 1).ISOWeek()
if t.Month() == time.January && t.Day() == 1 {
week = 1
}
}
dateStamp = strings.Replace(dateStamp, "%U", fmt.Sprintf("%02d", week), 1)
}
return dateStamp
}