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>
This commit is contained in:
2026-04-28 21:52:25 +02:00
co-authored by Claude Opus 4.7
parent b816914b9c
commit df9407dd8d
63 changed files with 7391 additions and 0 deletions
+427
View File
@@ -0,0 +1,427 @@
package notesium
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
const usage = `Usage: notesium COMMAND [OPTIONS]
Commands:
home Print path to notes directory
new Print path for a new note
--verbose Output key:value pairs of related info
--ctime= Use specified ctime instead of now (YYYY-MM-DDThh:mm:ss)
list Print list of notes
--color Color code prefix using ansi escape sequences
--labels Limit list to only label notes (ie. one word title)
--orphans Limit list to notes without outgoing or incoming links
--sort=WORD Sort list by date or alphabetically (ctime|mtime|alpha)
--prefix=WORD Prefix title with date or linked label (ctime|mtime|label)
--date=FORMAT Date format for ctime/mtime prefix (default: 2006-01-02)
links [filename] Print list of links
--color Color code using ansi escape sequences
--outgoing Limit list to outgoing links related to filename
--incoming Limit list to incoming links related to filename
--dangling Limit list to broken links
lines Print all lines of notes (ie. fulltext search)
--color Color code prefix using ansi escape sequences
--prefix=title Prefix each line with note title
--filter=QUERY Filter lines by query: AND (space), OR (|), NOT (!)
stats Print statistics
--color Color code using ansi escape sequences
--table Format as table with whitespace delimited columns
finder Start finder (interactive filter selection TUI)
--preview Display note preview (toggle with ctrl-/)
--prompt=STR Set custom prompt text
-- CMD [OPTS] Input (default: list --color --prefix=label --sort=alpha)
web Start web server
--webroot=PATH Path to web root to serve (default: embedded webroot)
--mount=DIR:URI Additional directory to serve under webroot (experimental)
--open-browser Launch default web browser with web server URL
--stop-on-idle Automatically stop when no activity is detected
--port=INT Port for web server to listen on (default: random)
--no-check Disable daily new version checks
--writable Allow writing of notes in NOTESIUM_DIR via API
extract [path] Print list of embedded files or contents of file path
version Print version
--verbose Output key:value pairs of related info
--check Check if a newer version is available
Environment:
NOTESIUM_DIR Path to notes directory (default: $HOME/notes)
`
type Command struct {
Name string
Options any
}
type newOptions struct {
ctime string
verbose bool
}
type listOptions struct {
color Color
limit string
prefix string
sortBy string
dateFormat string
}
type linksOptions struct {
color Color
limit string
filename string
}
type linesOptions struct {
color Color
prefix string
filter string
}
type finderOptions struct {
input []string
prompt string
preview bool
}
type catOptions struct {
filename string
lineNumber int
}
type statsOptions struct {
color Color
table bool
}
type webOptions struct {
host string
port int
webroot string
heartbeat bool
launchBrowser bool
readOnly bool
check bool
mounts map[string]string
}
type extractOptions struct {
path string
}
type versionOptions struct {
verbose bool
check bool
}
type Color struct {
Code string
Reset string
}
func parseOptions(args []string) (Command, error) {
if len(args) < 1 {
return Command{Name: "help"}, nil
}
cmd := Command{Name: args[0]}
switch cmd.Name {
case "-h", "--help", "help":
cmd.Name = "help"
return cmd, nil
// backwards compat.
case "-v", "--version":
cmd.Name = "version"
cmd.Options = versionOptions{}
return cmd, nil
case "home":
if len(args) > 1 {
return cmd, fmt.Errorf("unrecognized option: %s", args[1])
}
return cmd, nil
case "new":
opts := newOptions{}
for _, opt := range args[1:] {
switch {
case opt == "--verbose":
opts.verbose = true
case strings.HasPrefix(opt, "--ctime="):
opts.ctime = strings.TrimPrefix(opt, "--ctime=")
default:
return Command{}, fmt.Errorf("unrecognized option: %s", opt)
}
}
cmd.Options = opts
return cmd, nil
case "list":
opts := listOptions{}
opts.dateFormat = "2006-01-02"
for _, opt := range args[1:] {
switch {
case opt == "--color":
opts.color = defaultColor()
case opt == "--labels":
opts.limit = "labels"
case opt == "--orphans":
opts.limit = "orphans"
case opt == "--prefix=ctime":
opts.prefix = "ctime"
case opt == "--prefix=mtime":
opts.prefix = "mtime"
case opt == "--prefix=label":
opts.prefix = "label"
case opt == "--sort=ctime":
opts.sortBy = "ctime"
case opt == "--sort=mtime":
opts.sortBy = "mtime"
case opt == "--sort=alpha":
opts.sortBy = "alpha"
case strings.HasPrefix(opt, "--date="):
opts.dateFormat = strings.TrimPrefix(opt, "--date=")
default:
return Command{}, fmt.Errorf("unrecognized option: %s", opt)
}
}
cmd.Options = opts
return cmd, nil
case "links":
opts := linksOptions{}
filenameRequired := false
for _, opt := range args[1:] {
switch {
case opt == "--color":
opts.color = defaultColor()
case opt == "--dangling":
opts.limit = "dangling"
case opt == "--outgoing":
opts.limit = map[bool]string{true: "", false: "outgoing"}[opts.limit == "incoming"]
filenameRequired = true
case opt == "--incoming":
opts.limit = map[bool]string{true: "", false: "incoming"}[opts.limit == "outgoing"]
filenameRequired = true
case strings.HasPrefix(opt, "--filename=") && strings.HasSuffix(opt, ".md"):
opts.filename = strings.TrimPrefix(opt, "--filename=")
case strings.HasSuffix(opt, ".md"):
opts.filename = opt
default:
return Command{}, fmt.Errorf("unrecognized option: %s", opt)
}
}
if opts.filename != "" && opts.limit == "dangling" {
return Command{}, fmt.Errorf("filename not supported")
}
if opts.filename == "" && filenameRequired {
return Command{}, fmt.Errorf("filename is required")
}
cmd.Options = opts
return cmd, nil
case "lines":
opts := linesOptions{}
for _, opt := range args[1:] {
switch {
case opt == "--color":
opts.color = defaultColor()
case opt == "--prefix=title":
opts.prefix = "title"
case strings.HasPrefix(opt, "--filter="):
opts.filter = strings.TrimPrefix(opt, "--filter=")
default:
return Command{}, fmt.Errorf("unrecognized option: %s", opt)
}
}
cmd.Options = opts
return cmd, nil
case "finder":
opts := finderOptions{}
opts.input = []string{"list", "--color", "--prefix=label", "--sort=alpha"}
for i, opt := range args[1:] {
if opt == "--" {
opts.input = args[i+2:]
if len(opts.input) == 0 {
return Command{}, fmt.Errorf("input command not specified")
}
break
}
switch {
case opt == "--preview":
opts.preview = true
case strings.HasPrefix(opt, "--prompt="):
opts.prompt = strings.TrimPrefix(opt, "--prompt=")
default:
return Command{}, fmt.Errorf("unrecognized option: %s", opt)
}
}
cmd.Options = opts
return cmd, nil
case "stats":
opts := statsOptions{}
for _, opt := range args[1:] {
switch opt {
case "--color":
opts.color = defaultColor()
case "--table":
opts.table = true
default:
return Command{}, fmt.Errorf("unrecognized option: %s", opt)
}
}
cmd.Options = opts
return cmd, nil
case "web":
opts := webOptions{}
opts.host = "127.0.0.1"
opts.port = 0
opts.readOnly = true
opts.webroot = "embedded"
opts.check = true
opts.mounts = make(map[string]string)
for _, opt := range args[1:] {
switch {
case strings.HasPrefix(opt, "--webroot="):
webrootStr := strings.TrimPrefix(opt, "--webroot=")
webrootAbs, err := getAbsDir(webrootStr)
if err != nil {
return Command{}, fmt.Errorf("webroot %v: %s", err, webrootAbs)
}
opts.webroot = webrootAbs
case opt == "--open-browser":
opts.launchBrowser = true
case opt == "--stop-on-idle":
opts.heartbeat = true
case strings.HasPrefix(opt, "--port="):
portStr := strings.TrimPrefix(opt, "--port=")
port, err := strconv.Atoi(portStr)
if err != nil || port < 1024 || port > 65535 {
return Command{}, fmt.Errorf("invalid or out of range port number: %s", portStr)
}
opts.port = port
case opt == "--no-check":
opts.check = false
case opt == "--writable":
opts.readOnly = false
case strings.HasPrefix(opt, "--mount="):
mountStr := strings.TrimPrefix(opt, "--mount=")
mountPattern := regexp.MustCompile(`^(.+?):(/[a-zA-Z0-9-_]+/)$`)
matches := mountPattern.FindStringSubmatch(mountStr)
if matches == nil {
return Command{}, fmt.Errorf("mount format mismatch: expected '%s'", mountPattern.String())
}
srcAbs, err := getAbsDir(matches[1])
if err != nil {
return Command{}, fmt.Errorf("mount source %v: %s", err, srcAbs)
}
opts.mounts[matches[2]] = srcAbs
default:
return Command{}, fmt.Errorf("unrecognized option: %s", opt)
}
}
cmd.Options = opts
return cmd, nil
case "cat":
opts := catOptions{}
opts.lineNumber = 0
if len(args) != 2 {
return Command{}, fmt.Errorf("filename not specified or too many arguments")
}
parts := strings.Split(args[1], ":")
opts.filename = parts[0]
if len(parts) > 1 {
if num, err := strconv.Atoi(parts[1]); err == nil && num > 0 {
opts.lineNumber = num
}
}
cmd.Options = opts
return cmd, nil
case "version":
opts := versionOptions{}
for _, opt := range args[1:] {
switch opt {
case "--verbose":
opts.verbose = true
case "--check":
opts.check = true
default:
return Command{}, fmt.Errorf("unrecognized option: %s", opt)
}
}
cmd.Options = opts
return cmd, nil
case "extract":
opts := extractOptions{}
for _, opt := range args[1:] {
opts.path = opt
}
cmd.Options = opts
return cmd, nil
default:
if strings.HasPrefix(cmd.Name, "-") {
return cmd, fmt.Errorf("unrecognized option: %s", cmd.Name)
}
return cmd, fmt.Errorf("unrecognized command: %s", cmd.Name)
}
}
func getNotesiumDir() (string, error) {
dir, exists := os.LookupEnv("NOTESIUM_DIR")
if !exists {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
dir = filepath.Join(home, "notes")
}
absDir, err := getAbsDir(dir)
if err != nil {
return "", fmt.Errorf("NOTESIUM_DIR %v: %s", err, absDir)
}
return absDir, nil
}
func getAbsDir(dir string) (string, error) {
absDir, err := filepath.Abs(dir)
if err != nil {
return dir, fmt.Errorf("failed to resolve absolute path: %v", err)
}
realDir, err := filepath.EvalSymlinks(absDir)
if err != nil {
return absDir, fmt.Errorf("does not exist")
}
info, err := os.Stat(realDir)
if err != nil {
return realDir, fmt.Errorf("does not exist")
}
if !info.IsDir() {
return realDir, fmt.Errorf("is not a directory")
}
return realDir, nil
}
func defaultColor() Color {
return Color{
Code: "\033[0;36m",
Reset: "\033[0m",
}
}