package cmd import ( "fmt" "os" "strconv" "github.com/spf13/cobra" "github.com/yeho/klp/internal/clipboard" "github.com/yeho/klp/internal/config" "github.com/yeho/klp/internal/database" "github.com/yeho/klp/internal/search" "github.com/yeho/klp/internal/tui" ) var ( Version = "dev" // Set at build time via ldflags versionFlag bool listFlag bool listLimitFlag int limitFlag int searchFlag string deleteFlag string cfg *config.Config ) var rootCmd = &cobra.Command{ Use: "klp [id]", Short: "A clipboard history manager for Linux", Long: `klp is a clipboard history manager that monitors clipboard changes and provides quick access to your clipboard history.`, Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { if versionFlag { fmt.Println(Version) return } if deleteFlag != "" { deleteEntry(deleteFlag) return } if searchFlag != "" { searchEntries(searchFlag) return } // If ID provided as argument, copy that entry to clipboard if len(args) == 1 { copyEntryToClipboard(args[0]) return } if listFlag { // Non-interactive list limit := cfg.DefaultLimit if listLimitFlag > 0 { limit = listLimitFlag } listEntriesNonInteractive(limit) return } // Default: interactive list limit := cfg.DefaultLimit if limitFlag > 0 { limit = limitFlag } listEntriesInteractive(limit) }, } func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func init() { cobra.OnInitialize(initConfig) rootCmd.Flags().BoolVarP(&versionFlag, "version", "v", false, "Print version") rootCmd.Flags().BoolVarP(&listFlag, "list", "l", false, "Non-interactive list output") rootCmd.Flags().IntVarP(&listLimitFlag, "list-limit", "n", 0, "Limit for non-interactive list (use with -l)") rootCmd.Flags().IntVar(&limitFlag, "limit", 0, "Limit for interactive list") rootCmd.Flags().StringVarP(&searchFlag, "search", "s", "", "Search clipboard entries") rootCmd.Flags().StringVarP(&deleteFlag, "delete", "d", "", "Delete entry by ID") } 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 loadDatabase() *database.Database { db, err := database.Load(cfg.DBPath()) if err != nil { fmt.Fprintf(os.Stderr, "Error loading database: %v\n", err) os.Exit(1) } return db } func copyEntryToClipboard(id string) { db := loadDatabase() entry, err := db.Get(id) if err != nil { fmt.Fprintf(os.Stderr, "Entry not found: %s\n", id) os.Exit(1) } clip, err := clipboard.New() if err != nil { fmt.Fprintf(os.Stderr, "Error initializing clipboard: %v\n", err) os.Exit(1) } if err := clip.Write(entry.Value); err != nil { fmt.Fprintf(os.Stderr, "Error writing to clipboard: %v\n", err) os.Exit(1) } preview := entry.Value if len(preview) > 50 { preview = preview[:50] + "..." } fmt.Printf("Copied to clipboard: %s\n", cleanForDisplay(preview)) } func deleteEntry(id string) { db := loadDatabase() if err := db.Delete(id); err != nil { fmt.Fprintf(os.Stderr, "Error deleting entry: %v\n", err) os.Exit(1) } fmt.Printf("Deleted entry: %s\n", id) } func searchEntries(query string) { db := loadDatabase() searcher := search.NewSearcher(db) limit := cfg.DefaultLimit if limitFlag > 0 { limit = limitFlag } results := searcher.Search(query, search.Options{MaxResults: limit}) if len(results) == 0 { fmt.Printf("No results found for '%s'\n", query) return } // Convert results to entries for display entries := make([]*database.Entry, len(results)) for i, r := range results { entries[i] = r.Entry } if listFlag { // Non-interactive search results printEntries(entries) return } // Interactive search results title := fmt.Sprintf("Search results for '%s' (%d found)", query, len(results)) selected, err := tui.Run(entries, db, title) if err != nil { fmt.Fprintf(os.Stderr, "Error running TUI: %v\n", err) os.Exit(1) } if selected != nil { copyToClipboard(selected) } } func listEntriesNonInteractive(limit int) { db := loadDatabase() entries := db.List(limit) if len(entries) == 0 { fmt.Println("No clipboard entries found") return } printEntries(entries) } func listEntriesInteractive(limit int) { db := loadDatabase() entries := db.List(limit) if len(entries) == 0 { fmt.Println("No clipboard entries found") return } title := fmt.Sprintf("Clipboard History (%d entries)", len(entries)) selected, err := tui.Run(entries, db, title) if err != nil { fmt.Fprintf(os.Stderr, "Error running TUI: %v\n", err) os.Exit(1) } if selected != nil { copyToClipboard(selected) } } func printEntries(entries []*database.Entry) { for i, entry := range entries { preview := entry.Value if len(preview) > 60 { preview = preview[:60] + "..." } preview = cleanForDisplay(preview) fmt.Printf("%s. [%s] %s %s\n", padLeft(strconv.Itoa(i+1), 2), entry.ID, entry.DateTime.Format("01/02 15:04"), preview, ) } } func copyToClipboard(entry *database.Entry) { clip, err := clipboard.New() if err != nil { fmt.Fprintf(os.Stderr, "Error initializing clipboard: %v\n", err) os.Exit(1) } if err := clip.Write(entry.Value); err != nil { fmt.Fprintf(os.Stderr, "Error writing to clipboard: %v\n", err) os.Exit(1) } preview := entry.Value if len(preview) > 50 { preview = preview[:50] + "..." } fmt.Printf("Copied to clipboard: %s\n", cleanForDisplay(preview)) } func cleanForDisplay(s string) string { result := make([]byte, 0, len(s)) for i := 0; i < len(s); i++ { if s[i] == '\n' || s[i] == '\r' { result = append(result, ' ') } else if s[i] == '\t' { result = append(result, ' ') } else { result = append(result, s[i]) } } return string(result) } func padLeft(s string, width int) string { for len(s) < width { s = " " + s } return s }