This commit is contained in:
Levi Neely 2025-11-30 13:14:51 +01:00
parent 77821a7d01
commit 05e528e1e4
7 changed files with 193 additions and 21 deletions

View File

@ -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:<query>` style "links" with plumbing (e.g., `denote-query:project`). Possible approach:

View File

@ -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

View File

@ -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 <query>)
ctl (write-only) Control file (commands: filter <query>, cd <path>)
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/<identifier>/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/<identifier>/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

26
main.go
View File

@ -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)
}

3
mkfile
View File

@ -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
rm -f $HOME/bin/Denote $HOME/bin/Drn $HOME/bin/Djournal $HOME/bin/Dmerge $HOME/bin/Dbkp $HOME/bin/Dsilo

16
pkg/config/config.go Normal file
View File

@ -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")

28
scripts/Dsilo Executable file
View File

@ -0,0 +1,28 @@
#!/usr/local/plan9/bin/rc
# Dsilo - Switch denote directory (silo)
# Usage: Dsilo <directory-path>
# 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
}