diff --git a/README.md b/README.md index b02019c..0c42001 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,17 @@ This installs `Denote` and all its extensions to $HOME/bin. ### Setup -Start the Denote program by middle-clicking `Denote` anywhere in acme. This will open the `/Denote/` window. Notes are stored in `~/doc` by default, simply edit this in `main.go` to change your denote base directory. +Start the Denote program by middle-clicking `Denote` anywhere in acme. This will open the `/Denote/` window. + +**Configuration:** + +Notes are stored in `~/doc` by default. To change the default denote directory, edit `pkg/config/config.go`: + +```go +var DefaultDenoteDir = os.Getenv("HOME") + "/doc" +``` + +You can also switch between different directories at runtime using the `Dsilo` command (see [Dsilo](#dsilo)). ## Usage @@ -365,6 +375,32 @@ Dbkp 20251127T140558 The backup is extracted to a timestamped subdirectory within the restore directory, making it easy to identify when the backup was created. +### Dsilo + +Concept from prot's [denote-silo](https://github.com/protesilaos/denote-silo). Switch between different denote directories (silos) at runtime without restarting the program. + +**Usage:** + +Show the current silo: +``` +Dsilo +``` + +Switch to a different silo: +``` +Dsilo /path/to/another/directory +``` + +**What happens when you switch silos:** + +- The 9P filesystem immediately points to the new directory +- All existing notes are cleared from memory +- New notes are loaded from the new directory +- All subsequent operations (create, update, delete, search) use the new directory +- The Acme log watcher tracks files in the new directory + +This is useful for maintaining separate collections of notes (e.g., personal notes, work notes, project notes) and switching between them seamlessly. + ## Possible Future Work ### Templates @@ -382,13 +418,6 @@ Not a core feature of acme-denote, but could be implemented as an extension (e.g Currently, it is possible to simply grep inside of the denote directory to perform a content search. If this is not convenient enough, then adding `Grep` to the `/Denote/` window tag line and performing this search transparently would not be difficult. -### Multiple Denote Directories - -I find that it is enough to have one denote directory, but support multiple directories could be a nice feature. A possible approach is to keep the built-in directory (e.g., `$HOME/doc`) as the default, and pass in `path/to/dir` as an argument when running Denote. This could look like: - -- Highlight a directory path in acme -- `2-1` chord to Denote points Denote to that directory instead of the default - ### Support Query "Links" Support `denote-query:` style "links" with plumbing (e.g., `denote-query:project`). Possible approach: diff --git a/internal/disk/sync.go b/internal/disk/sync.go index e05e853..e1941fa 100644 --- a/internal/disk/sync.go +++ b/internal/disk/sync.go @@ -7,8 +7,9 @@ the file name. package disk import ( - "denote/pkg/metadata" p9client "denote/internal/p9/client" + "denote/pkg/config" + "denote/pkg/metadata" "fmt" "log" "os" @@ -79,7 +80,18 @@ func WatchAcmeLog() { // isDenoteFile checks if the file is in the denote directory func isDenoteFile(path string) bool { - denoteDir := os.Getenv("HOME") + "/doc" + // Try to read current dir from 9p, fall back to default + denoteDir := config.DefaultDenoteDir + + err := p9client.With9P(func(f *client.Fsys) error { + dir, readErr := p9client.ReadFile(f, "dir") + if readErr == nil && dir != "" { + denoteDir = strings.TrimSpace(dir) + } + return nil // Don't propagate error, just use fallback + }) + _ = err // Ignore connection errors, use default + absPath, err := filepath.Abs(path) if err != nil { return false diff --git a/internal/p9/server/server.go b/internal/p9/server/server.go index 4579d36..df8b62f 100644 --- a/internal/p9/server/server.go +++ b/internal/p9/server/server.go @@ -4,7 +4,7 @@ denote metadata for all files in the denote directory. The filesystem is organiz follows: denote/ (directory) Root filesystem - ctl (write-only) Control file (commands: filter ) + ctl (write-only) Control file (commands: filter , cd ) event (read-only) Global event bus (listen & react to rename, update, and delete events) index (read-only) Metadata index (respects active filter) new (write-only) Create new note (write "'title' tag1,tag2") @@ -17,12 +17,14 @@ follows: title (read-write) Denote document title Notes: -- Messages written to denote/ctl affect global state (e.g., filtering) +- Messages written to denote/ctl affect global state (e.g., filtering, directory switching) - Filter command syntax: filter [field:]pattern where field is date|title|tag - Multiple filters can be specified: filter tag:journal !tag:draft - Titles with spaces must be quoted: filter title:"My Title" - Writing 'filter' with no arguments clears the active filter - Index respects the current filter - returns only matching notes +- Cd command syntax: cd /path/to/directory +- After cd, run Get to reload notes from the new directory - Messages written to denote/n//ctl may generate events that are broadcast over the global event bus - Writing to title or keywords triggers an update ('u') event and a rename ('r') event - Write 'd' to denote/n//ctl to delete a denote file @@ -39,6 +41,7 @@ import ( "io" "net" "os" + "path/filepath" "regexp" "strings" "sync" @@ -104,6 +107,17 @@ type fid struct { var srv *server +// GetDenoteDir returns the current denote directory. +// Returns empty string if server not yet initialized. +func GetDenoteDir() string { + if srv == nil { + return "" + } + srv.mu.RLock() + defer srv.mu.RUnlock() + return srv.denoteDir +} + // findNote finds a note by identifier in the in-memory collection func findNote(identifier string) (*metadata.Metadata, error) { srv.mu.RLock() @@ -971,6 +985,10 @@ func (s *server) handleCtlCommand(fc *plan9.Fcall) *plan9.Fcall { return s.handleFilterCommand(fc, command) } + if strings.HasPrefix(command, "cd ") { + return s.handleCdCommand(fc, command) + } + return errorFcall(fc, fmt.Sprintf("unknown ctl command: %s", command)) } @@ -1026,6 +1044,64 @@ func (s *server) handleFilterCommand(fc *plan9.Fcall, command string) *plan9.Fca } } +// handleCdCommand processes the 'cd' ctl command to change denote directory +func (s *server) handleCdCommand(fc *plan9.Fcall, command string) *plan9.Fcall { + // Extract path after "cd " keyword + newPath := strings.TrimSpace(strings.TrimPrefix(command, "cd ")) + + if newPath == "" { + return errorFcall(fc, "cd: path required") + } + + // Expand ~ to home directory + if strings.HasPrefix(newPath, "~") { + home := os.Getenv("HOME") + if home == "" { + return errorFcall(fc, "cd: cannot expand ~ (HOME not set)") + } + newPath = strings.Replace(newPath, "~", home, 1) + } + + // Clean the path + newPath = filepath.Clean(newPath) + + // Convert to absolute path + absPath, err := filepath.Abs(newPath) + if err != nil { + return errorFcall(fc, fmt.Sprintf("cd: invalid path: %v", err)) + } + + // Verify directory exists + info, err := os.Stat(absPath) + if err != nil { + return errorFcall(fc, fmt.Sprintf("cd: %v", err)) + } + if !info.IsDir() { + return errorFcall(fc, fmt.Sprintf("cd: %s is not a directory", absPath)) + } + + // Load metadata from new directory + newNotes, err := disk.LoadAll(absPath) + if err != nil { + return errorFcall(fc, fmt.Sprintf("cd: failed to load directory: %v", err)) + } + + // Update server state atomically + s.mu.Lock() + s.denoteDir = absPath + s.notes = newNotes + // Clear filter state when switching directories + s.filterQuery = "" + s.filteredNotes = nil + s.mu.Unlock() + + return &plan9.Fcall{ + Type: plan9.Rwrite, + Tag: fc.Tag, + Count: uint32(len(fc.Data)), + } +} + // parseFilterQuery parses a filter command query into Filter objects // Handles quoted strings for titles: title:"my title" or title:'my title' // Multiple filters space-separated: tag:journal !tag:draft diff --git a/main.go b/main.go index 91ac423..dfacd6b 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "denote/internal/disk" p9client "denote/internal/p9/client" p9server "denote/internal/p9/server" + "denote/pkg/config" "denote/pkg/encoding/frontmatter" "denote/pkg/encoding/results" "denote/pkg/metadata" @@ -26,7 +27,15 @@ const ( ftype = metadata.FileTypeMdYaml ) -var denoteDir = os.Getenv("HOME") + "/doc" +// getDenoteDir returns the current denote directory. +// Uses the server's current directory if available, +// otherwise falls back to DefaultDenoteDir. +func getDenoteDir() string { + if dir := p9server.GetDenoteDir(); dir != "" { + return dir + } + return config.DefaultDenoteDir +} // Helper functions for 9P operations @@ -67,7 +76,8 @@ func setFilter(f *client.Fsys, filterQuery string) error { // handleNewEvent is called when a new note is created. // When a new note is created in 9P, open an acme window (file created on Put). -func handleNewEvent(f *client.Fsys, identifier, denoteDir string) error { +func handleNewEvent(f *client.Fsys, identifier string) error { + denoteDir := getDenoteDir() fields, err := p9client.ReadFields(f, identifier, "title", "keywords", "signature") if err != nil { return fmt.Errorf("failed to get metadata: %w", err) @@ -212,7 +222,7 @@ func main() { } // Load metadata from filesystem - notes, err := disk.LoadAll(denoteDir) + notes, err := disk.LoadAll(config.DefaultDenoteDir) if err != nil { fmt.Fprintf(os.Stderr, "warning: failed to load notes: %v\n", err) } @@ -221,28 +231,28 @@ func main() { callbacks := p9server.Callbacks{ OnNew: func(identifier string) error { return p9client.With9P(func(f *client.Fsys) error { - return handleNewEvent(f, identifier, denoteDir) + return handleNewEvent(f, identifier) }) }, OnUpdate: func(identifier string) error { return p9client.With9P(func(f *client.Fsys) error { - return disk.HandleUpdateEvent(f, identifier, denoteDir) + return disk.HandleUpdateEvent(f, identifier, getDenoteDir()) }) }, OnRename: func(identifier string) error { return p9client.With9P(func(f *client.Fsys) error { - return disk.HandleRenameEvent(f, identifier, denoteDir) + return disk.HandleRenameEvent(f, identifier, getDenoteDir()) }) }, OnDelete: func(identifier string) error { return p9client.With9P(func(f *client.Fsys) error { - return disk.HandleDeleteEvent(f, identifier, denoteDir) + return disk.HandleDeleteEvent(f, identifier, getDenoteDir()) }) }, } // 9p server startup with pre-loaded data and callbacks - if err := p9server.StartServer(notes, denoteDir, callbacks); err != nil { + if err := p9server.StartServer(notes, config.DefaultDenoteDir, callbacks); err != nil { log.Fatal(err) } diff --git a/mkfile b/mkfile index d9153ed..d925809 100644 --- a/mkfile +++ b/mkfile @@ -4,6 +4,7 @@ install:V: cp scripts/Djournal $HOME/bin/Djournal cp scripts/Dmerge $HOME/bin/Dmerge cp scripts/Dbkp $HOME/bin/Dbkp + cp scripts/Dsilo $HOME/bin/Dsilo clean:V: - rm -f $HOME/bin/Denote $HOME/bin/Drn $HOME/bin/Djournal $HOME/bin/Dmerge $HOME/bin/Dbkp \ No newline at end of file + rm -f $HOME/bin/Denote $HOME/bin/Drn $HOME/bin/Djournal $HOME/bin/Dmerge $HOME/bin/Dbkp $HOME/bin/Dsilo \ No newline at end of file diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..418ca1b --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,16 @@ +package config + +import "os" + +// ============================================================ +// CONFIGURATION: Default Denote Directory +// +// Change this value to set your default denote directory. +// This is used at program startup. After startup, use the +// Dsilo command to switch between directories dynamically. +// ============================================================ +var DefaultDenoteDir = os.Getenv("HOME") + "/doc" + +// Examples of alternative configurations: +// var DefaultDenoteDir = "/home/lkn/notes" +// var DefaultDenoteDir = os.Getenv("DENOTE_DIR") diff --git a/scripts/Dsilo b/scripts/Dsilo new file mode 100755 index 0000000..d5ca566 --- /dev/null +++ b/scripts/Dsilo @@ -0,0 +1,28 @@ +#!/usr/local/plan9/bin/rc + +# Dsilo - Switch denote directory (silo) +# Usage: Dsilo +# Dsilo (no args - show current silo) + +if(~ $#* 1) { + newdir=$1 + + # Validate directory exists + if(! test -d $newdir) { + echo 'error: directory does not exist:' $newdir >[1=2] + exit 1 + } + + # Get absolute path + absdir=`{cd $newdir && pwd} + + # Write cd command to ctl file + echo 'cd '$absdir | 9p write denote/ctl && { + echo 'Switched to silo:' $absdir '. You may have to refresh the Denote window.' + exit 0 + } +} +if not { + echo 'Denote Directory:' `{9p read denote/dir} + exit 0 +}