init commit
This commit is contained in:
159
cmd/add.go
Normal file
159
cmd/add.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/yeho/doks/internal/registry"
|
||||
"github.com/yeho/doks/internal/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
addKey string
|
||||
addPreserve bool
|
||||
addTags string
|
||||
addDescription string
|
||||
)
|
||||
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add <path>",
|
||||
Short: "Add a document to doks",
|
||||
Long: `Add a document to doks by copying it to the storage directory.
|
||||
Use -p to create a symlink at the original location pointing to the stored file.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
filePath := args[0]
|
||||
|
||||
if addKey == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: -k (key) flag is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
absPath, err := storage.GetAbsolutePath(filePath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving path: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Error: file not found: %s\n", absPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Load registry
|
||||
reg, err := registry.Load(cfg.RegistryPath())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading registry: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Check if key already exists
|
||||
if reg.Exists(addKey) {
|
||||
fmt.Fprintf(os.Stderr, "Error: key '%s' already exists\n", addKey)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Generate unique filename
|
||||
filename := storage.GenerateFilename(absPath)
|
||||
|
||||
// Create storage and copy file
|
||||
store := storage.New(cfg.FilesDir())
|
||||
if err := store.CopyFile(absPath, filename); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error copying file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Remove original and create symlink if preserve flag is set
|
||||
if addPreserve {
|
||||
if err := os.Remove(absPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error removing original file: %v\n", err)
|
||||
// Clean up copied file
|
||||
store.RemoveFile(filename)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := store.CreateSymlink(absPath, filename); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating symlink: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse tags
|
||||
var tags []string
|
||||
if addTags != "" {
|
||||
tags = splitTags(addTags)
|
||||
}
|
||||
|
||||
// Create registry entry
|
||||
entry := ®istry.Entry{
|
||||
Key: addKey,
|
||||
Filename: filename,
|
||||
OriginalPath: absPath,
|
||||
HasSymlink: addPreserve,
|
||||
Tags: tags,
|
||||
Description: addDescription,
|
||||
}
|
||||
|
||||
if err := reg.Add(entry); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error adding to registry: %v\n", err)
|
||||
// Clean up
|
||||
store.RemoveFile(filename)
|
||||
if addPreserve {
|
||||
store.RemoveSymlink(absPath)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Added '%s' with key '%s'\n", filePath, addKey)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(addCmd)
|
||||
addCmd.Flags().StringVarP(&addKey, "key", "k", "", "Unique key for the document (required)")
|
||||
addCmd.Flags().BoolVarP(&addPreserve, "preserve", "p", false, "Create symlink at original location")
|
||||
addCmd.Flags().StringVarP(&addTags, "tags", "t", "", "Comma-separated tags")
|
||||
addCmd.Flags().StringVarP(&addDescription, "description", "d", "", "Description of the document")
|
||||
addCmd.MarkFlagRequired("key")
|
||||
}
|
||||
|
||||
func splitTags(tags string) []string {
|
||||
if tags == "" {
|
||||
return nil
|
||||
}
|
||||
result := []string{}
|
||||
for _, tag := range splitString(tags, ",") {
|
||||
tag = trimSpace(tag)
|
||||
if tag != "" {
|
||||
result = append(result, tag)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func splitString(s, sep string) []string {
|
||||
var result []string
|
||||
start := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
if i+len(sep) <= len(s) && s[i:i+len(sep)] == sep {
|
||||
result = append(result, s[start:i])
|
||||
start = i + len(sep)
|
||||
}
|
||||
}
|
||||
result = append(result, s[start:])
|
||||
return result
|
||||
}
|
||||
|
||||
func trimSpace(s string) string {
|
||||
start := 0
|
||||
end := len(s)
|
||||
for start < end && (s[start] == ' ' || s[start] == '\t') {
|
||||
start++
|
||||
}
|
||||
for end > start && (s[end-1] == ' ' || s[end-1] == '\t') {
|
||||
end--
|
||||
}
|
||||
return s[start:end]
|
||||
}
|
||||
59
cmd/edit.go
Normal file
59
cmd/edit.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/yeho/doks/internal/registry"
|
||||
)
|
||||
|
||||
var editCmd = &cobra.Command{
|
||||
Use: "edit <key>",
|
||||
Short: "Edit a document in your configured editor",
|
||||
Long: `Open a document in your configured editor (default: vim). Set defaultEditor in config to change.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: completeKeys,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
key := args[0]
|
||||
|
||||
reg, err := registry.Load(cfg.RegistryPath())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading registry: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
entry, err := reg.Get(key)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Document not found: %s\n", key)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
filePath := cfg.FilePath(entry.Filename)
|
||||
|
||||
// Get editor from config, fallback to EDITOR env, then vim
|
||||
editor := cfg.DefaultEditor
|
||||
if editor == "" {
|
||||
editor = os.Getenv("EDITOR")
|
||||
}
|
||||
if editor == "" {
|
||||
editor = "vim"
|
||||
}
|
||||
|
||||
// Run editor
|
||||
editorCmd := exec.Command(editor, filePath)
|
||||
editorCmd.Stdin = os.Stdin
|
||||
editorCmd.Stdout = os.Stdout
|
||||
editorCmd.Stderr = os.Stderr
|
||||
|
||||
if err := editorCmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error running editor: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(editCmd)
|
||||
}
|
||||
93
cmd/list.go
Normal file
93
cmd/list.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/yeho/doks/internal/registry"
|
||||
)
|
||||
|
||||
var listTagFilter string
|
||||
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all registered documents",
|
||||
Long: `Display all documents registered in doks, optionally filtered by tag.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
reg, err := registry.Load(cfg.RegistryPath())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading registry: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
entries := reg.List()
|
||||
if len(entries) == 0 {
|
||||
fmt.Println("No documents registered")
|
||||
return
|
||||
}
|
||||
|
||||
// Sort by key
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].Key < entries[j].Key
|
||||
})
|
||||
|
||||
// Filter by tag if specified
|
||||
if listTagFilter != "" {
|
||||
filtered := make([]*registry.Entry, 0)
|
||||
for _, entry := range entries {
|
||||
if hasTag(entry.Tags, listTagFilter) {
|
||||
filtered = append(filtered, entry)
|
||||
}
|
||||
}
|
||||
entries = filtered
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
fmt.Printf("No documents found with tag '%s'\n", listTagFilter)
|
||||
return
|
||||
}
|
||||
|
||||
// Print entries
|
||||
for _, entry := range entries {
|
||||
symlink := ""
|
||||
if entry.HasSymlink {
|
||||
symlink = " [symlinked]"
|
||||
}
|
||||
tags := ""
|
||||
if len(entry.Tags) > 0 {
|
||||
tags = fmt.Sprintf(" [%s]", joinStrings(entry.Tags, ", "))
|
||||
}
|
||||
fmt.Printf(" %s%s%s\n", entry.Key, tags, symlink)
|
||||
if entry.Description != "" {
|
||||
fmt.Printf(" %s\n", entry.Description)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(listCmd)
|
||||
listCmd.Flags().StringVarP(&listTagFilter, "tag", "t", "", "Filter by tag")
|
||||
}
|
||||
|
||||
func hasTag(tags []string, target string) bool {
|
||||
for _, tag := range tags {
|
||||
if tag == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func joinStrings(strs []string, sep string) string {
|
||||
if len(strs) == 0 {
|
||||
return ""
|
||||
}
|
||||
result := strs[0]
|
||||
for i := 1; i < len(strs); i++ {
|
||||
result += sep + strs[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
71
cmd/remove.go
Normal file
71
cmd/remove.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/yeho/doks/internal/registry"
|
||||
"github.com/yeho/doks/internal/storage"
|
||||
)
|
||||
|
||||
var removeDelete bool
|
||||
|
||||
var removeCmd = &cobra.Command{
|
||||
Use: "remove <key>",
|
||||
Short: "Remove a document from doks",
|
||||
Long: `Remove a document from doks by its key.
|
||||
By default, only the registry entry is removed. Use --delete to also delete the stored file.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: completeKeys,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
key := args[0]
|
||||
|
||||
// Load registry
|
||||
reg, err := registry.Load(cfg.RegistryPath())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading registry: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get entry
|
||||
entry, err := reg.Get(key)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Document not found: %s\n", key)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
store := storage.New(cfg.FilesDir())
|
||||
|
||||
// Remove symlink if exists
|
||||
if entry.HasSymlink {
|
||||
if err := store.RemoveSymlink(entry.OriginalPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: could not remove symlink: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete file if requested
|
||||
if removeDelete {
|
||||
if err := store.RemoveFile(entry.Filename); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: could not delete file: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from registry
|
||||
if err := reg.Remove(key); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error removing from registry: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if removeDelete {
|
||||
fmt.Printf("Removed '%s' and deleted file\n", key)
|
||||
} else {
|
||||
fmt.Printf("Removed '%s' from registry (file kept in storage)\n", key)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(removeCmd)
|
||||
removeCmd.Flags().BoolVar(&removeDelete, "delete", false, "Also delete the stored file")
|
||||
}
|
||||
185
cmd/root.go
Normal file
185
cmd/root.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/yeho/doks/internal/config"
|
||||
"github.com/yeho/doks/internal/registry"
|
||||
"github.com/yeho/doks/internal/search"
|
||||
"github.com/yeho/doks/internal/tui"
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "dev" // Set at build time via ldflags
|
||||
|
||||
keyFlag string
|
||||
searchFlag string
|
||||
versionFlag bool
|
||||
cfg *config.Config
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "doks",
|
||||
Short: "A terminal app for quick access to personal notes",
|
||||
Long: `doks is a terminal application for quickly accessing short notes
|
||||
and manuals. Access documents by key or search through content.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if versionFlag {
|
||||
fmt.Println(Version)
|
||||
return
|
||||
}
|
||||
if keyFlag != "" {
|
||||
retrieveByKey(keyFlag)
|
||||
return
|
||||
}
|
||||
if searchFlag != "" {
|
||||
searchDocuments(searchFlag)
|
||||
return
|
||||
}
|
||||
cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
rootCmd.Flags().StringVarP(&keyFlag, "key", "k", "", "Retrieve document by key")
|
||||
rootCmd.Flags().StringVarP(&searchFlag, "search", "s", "", "Search documents by text")
|
||||
rootCmd.Flags().BoolVarP(&versionFlag, "version", "v", false, "Print version")
|
||||
|
||||
// Register key completion
|
||||
rootCmd.RegisterFlagCompletionFunc("key", completeKeys)
|
||||
}
|
||||
|
||||
func completeKeys(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
// Need to initialize config for completion
|
||||
if cfg == nil {
|
||||
var err error
|
||||
cfg, err = config.Load()
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
}
|
||||
|
||||
reg, err := registry.Load(cfg.RegistryPath())
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
||||
keys := []string{}
|
||||
for _, entry := range reg.List() {
|
||||
keys = append(keys, entry.Key)
|
||||
}
|
||||
return keys, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
var err error
|
||||
cfg, err = config.Load()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveByKey(key string) {
|
||||
reg, err := registry.Load(cfg.RegistryPath())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading registry: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
entry, err := reg.Get(key)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Document not found: %s\n", key)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
filePath := cfg.FilePath(entry.Filename)
|
||||
displayFile(filePath, 0)
|
||||
}
|
||||
|
||||
func searchDocuments(query string) {
|
||||
reg, err := registry.Load(cfg.RegistryPath())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading registry: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
searcher := search.NewTextSearcher(cfg.FilesDir(), reg)
|
||||
opts := search.Options{
|
||||
MaxResults: cfg.Display.MaxResults,
|
||||
ContextLines: cfg.Display.ContextLines,
|
||||
}
|
||||
|
||||
results, err := searcher.Search(query, opts)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error searching: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
fmt.Printf("No results found for '%s'\n", query)
|
||||
return
|
||||
}
|
||||
|
||||
// If only one result, show it directly
|
||||
if len(results) == 1 {
|
||||
showResult(&results[0])
|
||||
return
|
||||
}
|
||||
|
||||
// Multiple results - show TUI
|
||||
selected, err := tui.Run(results, query)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error running TUI: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if selected != nil {
|
||||
showResult(selected)
|
||||
}
|
||||
}
|
||||
|
||||
func showResult(result *search.Result) {
|
||||
fmt.Printf("--- %s (line %d) ---\n", result.Key, result.LineNumber)
|
||||
displayFile(result.FilePath, result.LineNumber)
|
||||
}
|
||||
|
||||
func displayFile(filePath string, highlightLine int) {
|
||||
// Try bat first for syntax highlighting
|
||||
if batPath, err := exec.LookPath("bat"); err == nil {
|
||||
args := []string{
|
||||
"--paging=never",
|
||||
"--color=always",
|
||||
}
|
||||
if highlightLine > 0 {
|
||||
args = append(args, fmt.Sprintf("--highlight-line=%d", highlightLine))
|
||||
}
|
||||
args = append(args, filePath)
|
||||
|
||||
cmd := exec.Command(batPath, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to plain output
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Print(string(content))
|
||||
}
|
||||
Reference in New Issue
Block a user