/* Copyright © 2025 NAME HERE */ package cmd import ( "bytes" "context" "fmt" "os" "path/filepath" "sync" "github.com/scrotadamus/ghligh/document" "github.com/spf13/cobra" ) var recursive bool type resolver struct { paths []string recurse bool ctx context.Context ch chan<- string wg sync.WaitGroup } func (r *resolver) resolve() { for _, path := range r.paths { r.wg.Add(1) go r.resolvePath(path) } go func() { r.wg.Wait() close(r.ch) }() } func (r *resolver) resolvePath(path string) { defer r.wg.Done() if err := r.ctx.Err(); err != nil { return } entries, err := os.ReadDir(path) if err != nil { str, err := filepath.Abs(path) if err != nil { return } r.ch <- str return } for _, entry := range entries { info, err := entry.Info() if err != nil { fmt.Fprintf(os.Stderr, "Error retrieving info for %s: %v\n", entry.Name(), err) continue } fullPath := filepath.Join(path, entry.Name()) if info.IsDir() { if r.recurse { if err := r.ctx.Err(); err != nil { return } r.wg.Add(1) go r.resolvePath(fullPath) } } else if info.Mode().IsRegular() { r.ch <- fullPath } } } // ArgsOrCWD returns the provided args slice if non-empty. // If args is empty, it returns a slice containing the current working directory. // // Example: // // ArgsOrCWD([]string{"path1", "file2"}) -> []string{"path1", "file2"} // ArgsOrCWD([]string{}) -> []string{"."} func ArgsOrCWD(args []string) []string { if len(args) == 0 { cwd, err := os.Getwd() if err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) return nil } return []string{cwd} } return args } // returns true if file specified in path start with the PDF magic header func isPDF(path string) bool { file, err := os.Open(path) if err != nil { // We only care about printing permissions errors // and things like that fmt.Fprintf(os.Stderr, "%s\n", err) return false } defer file.Close() header := make([]byte, 5) _, err = file.Read(header) if err != nil { return false } return bytes.Equal(header, []byte("%PDF-")) } // returns true if file contains at least one highlight or is tagged with "ls" (is ls-able by ghligh) func HasHighlights(path string) bool { // Ensure is a pdf file, might block for other kind of files if !isPDF(path) { return false } doc, err := document.Open(path) if err != nil { return false } defer doc.Close() return doc.HasHighlights() } // lsCmd represents the ls command var lsCmd = &cobra.Command{ Use: "ls", Short: "show files with highlights or tagged with 'ls' [unix]", Long: ` ghligh ls file1.pdf directory [-R] [-c] will show every file inside directory that contains highlights or it is marked ls with the ghligh tag add command ghligh ls # show files in current dir ghligh ls file1.pdf # if it outpus file1.pdf it means that file1.pdf contains highlights ghligh ls -c file1.pdf # same as ghligh ls file1.pdf but exit status will not be if file1.pdf doesnt contains highlights ghligh ls -R # do it recursively, be careful with symlink dir cycles, as I am to lazy to address that particular issue `, Run: func(cmd *cobra.Command, args []string) { files := ArgsOrCWD(args) ch := make(chan string) ctx := context.Background() res := resolver{ paths: files, recurse: recursive, ctx: ctx, ch: ch, } go res.resolve() var wg sync.WaitGroup var found bool for file := range ch { wg.Add(1) go func(f string) { defer wg.Done() if HasHighlights(f) { found = true fmt.Printf("%s\n", f) } }(file) } wg.Wait() check, err := cmd.Flags().GetBool("check") if err != nil { cmd.Help() return } if check && !found { os.Exit(1) } }, } func init() { rootCmd.AddCommand(lsCmd) lsCmd.Flags().BoolVarP(&recursive, "recursive", "R", false, "List recursively") lsCmd.Flags().BoolP("check", "c", false, "exit status is 1 if no file its found") // order pdf by time of something (modification / creation) ??? //lsCmd.Flags().BoolP("time", "t", false, "ls by time") }