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:
@@ -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",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user