init commit

This commit is contained in:
ysandler
2026-01-22 23:03:49 -06:00
commit 1eae04fc60
21 changed files with 1688 additions and 0 deletions

159
cmd/add.go Normal file
View 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 := &registry.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
View 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
View 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
View 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
View 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))
}