52160345bf
Complete implementation of note management CLI with all core features: Commands: - add: Create new notes in $EDITOR with auto-generated filenames - list: Display all notes with titles, paths, and tags - search: Full-text search via ripgrep, tag-based filtering - tags: List all tags with occurrence counts - edit: Fuzzy search and edit notes by title - rm: Move notes to trash with confirmation prompt Features: - Automatic depository structure initialization (.jade/trash/) - Configurable tag prefix (default '+') - Parse title from first # heading (filename fallback) - Extract tags anywhere in content - Parse both [[wiki-links]] and [markdown](links) - Trash system with timestamps to prevent conflicts Technical: - Global config at ~/.config/jade/config.yml - Per-depository settings support - Ripgrep integration for fast search - $EDITOR integration for note editing - Comprehensive README with usage examples
127 lines
3.0 KiB
Go
127 lines
3.0 KiB
Go
package engine
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// SearchContent performs a full-text search using ripgrep
|
|
func (jd *JadeDepo) SearchContent(query string) ([]string, error) {
|
|
// Check if ripgrep is available
|
|
if _, err := exec.LookPath("rg"); err != nil {
|
|
return nil, fmt.Errorf("ripgrep (rg) is not installed. Please install it to use search functionality")
|
|
}
|
|
|
|
// Run ripgrep to search for content
|
|
cmd := exec.Command("rg",
|
|
"--files-with-matches", // Only show filenames
|
|
"--glob", "*.md", // Only search markdown files
|
|
"--glob", "!.jade", // Exclude .jade directory
|
|
query,
|
|
jd.Config.DepoPath,
|
|
)
|
|
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
// ripgrep returns exit code 1 when no matches found
|
|
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
|
|
return []string{}, nil
|
|
}
|
|
return nil, fmt.Errorf("search failed: %w", err)
|
|
}
|
|
|
|
// Parse output into list of files
|
|
files := strings.Split(strings.TrimSpace(string(output)), "\n")
|
|
|
|
// Make paths relative to depo
|
|
var results []string
|
|
for _, file := range files {
|
|
if file != "" {
|
|
relPath, err := filepath.Rel(jd.Config.DepoPath, file)
|
|
if err != nil {
|
|
relPath = file
|
|
}
|
|
results = append(results, relPath)
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// SearchByTag searches for notes containing a specific tag
|
|
func (jd *JadeDepo) SearchByTag(tag string) ([]string, error) {
|
|
// Ensure tag has prefix
|
|
if !strings.HasPrefix(tag, jd.Config.TagPrefix) {
|
|
tag = jd.Config.TagPrefix + tag
|
|
}
|
|
|
|
// Use ripgrep to find the tag (escape special regex characters)
|
|
escapedTag := regexp.QuoteMeta(tag)
|
|
return jd.SearchContent(escapedTag)
|
|
}
|
|
|
|
// ListAllNotes returns all markdown notes in the depository
|
|
func (jd *JadeDepo) ListAllNotes() ([]*Note, error) {
|
|
var notes []*Note
|
|
|
|
err := filepath.Walk(jd.Config.DepoPath, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Skip .jade directory
|
|
if info.IsDir() && info.Name() == ".jade" {
|
|
return filepath.SkipDir
|
|
}
|
|
|
|
// Only process .md files
|
|
if !info.IsDir() && strings.HasSuffix(info.Name(), ".md") {
|
|
relPath, err := filepath.Rel(jd.Config.DepoPath, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
note, err := LoadNote(jd.Config.DepoPath, relPath, jd.Config.TagPrefix)
|
|
if err != nil {
|
|
// Log error but continue
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to load note %s: %v\n", relPath, err)
|
|
return nil
|
|
}
|
|
|
|
notes = append(notes, note)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list notes: %w", err)
|
|
}
|
|
|
|
return notes, nil
|
|
}
|
|
|
|
// FindNoteByTitle searches for a note by title (fuzzy match)
|
|
func (jd *JadeDepo) FindNoteByTitle(title string) ([]*Note, error) {
|
|
allNotes, err := jd.ListAllNotes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var matches []*Note
|
|
titleLower := strings.ToLower(title)
|
|
|
|
for _, note := range allNotes {
|
|
// Check if title contains the search term
|
|
if strings.Contains(strings.ToLower(note.Title), titleLower) {
|
|
matches = append(matches, note)
|
|
}
|
|
}
|
|
|
|
return matches, nil
|
|
}
|