Dsilo
This commit is contained in:
parent
77821a7d01
commit
05e528e1e4
45
README.md
45
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:<query>` style "links" with plumbing (e.g., `denote-query:project`). Possible approach:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
26
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
3
mkfile
3
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
|
||||
rm -f $HOME/bin/Denote $HOME/bin/Drn $HOME/bin/Djournal $HOME/bin/Dmerge $HOME/bin/Dbkp $HOME/bin/Dsilo
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue