package main import ( "bufio" "flag" "fmt" "os" "os/signal" "path/filepath" "strings" "sync" "syscall" "time" "github.com/fatih/color" ) var ( Current Entry Root Entry ColorRed = color.New(color.FgRed) ColorBlue = color.New(color.FgBlue) ColorGreen = color.New(color.FgGreen) IconTheme = flag.String("i", "unicode", "icon theme (currently supported: default, unicode, nerd)") ) // FIXME sometimes sorting not applied after removing items // TODO pwd command showing Current entry path and Root entry path in filesystem // TODO arrow up -> last command (`stty -raw` necessary?). even better: rlwrap impl (see tsoding noq) // TODO config file with preferred icon theme or read best icon theme from terminfo somehow // TODO on exit: show deleted files in recursive tree (only deleted files and their parent directories) // TODO autocomplete on TAB (see arrow up issue: raw necessary?) func main() { flag.Parse() rootPath := flag.Arg(0) mounts, err := Mounts() if err != nil { fmt.Fprintln(os.Stderr, "Mount points could not be read! All mount points will be calculated as well") } if abs, err := filepath.Abs(rootPath); err == nil { rootPath = abs } else { panic(err) } root, err := NewEntry(rootPath) if err != nil { panic(err) } start := time.Now() ScanEntryLive(root, mounts) fmt.Println(Translate("Scanning took %s", time.Since(start))) fmt.Println() c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { for range c { fmt.Println() showError("Use command 'exit' for exiting properly") fmt.Print("> ") } }() Root = root Current = root s := bufio.NewScanner(os.Stdin) showCurrentDir := true for { if showCurrentDir { fmt.Println(Current.StringRecursive(1)) } fmt.Print("> ") s.Scan() input := s.Text() showCurrentDir = handleInput(input) } } func handleInput(input string) bool { cmd := strings.Split(strings.TrimSpace(input), " ") switch cmd[0] { case "ls": ls(cmd[1:]) return false case "cd": cd(cmd[1:]) case "rm": rm(cmd[1:], true) case "urm": rm(cmd[1:], false) case "exit": exit() case "help": help(cmd[1:]) } return true } func showError(str string, args ...interface{}) { fmt.Println() ColorRed.Println(Translate(str, args...)) fmt.Println() } func entryByPath(path string) Entry { if !strings.HasPrefix(path, Root.Path()) { return nil } path = strings.TrimPrefix(path, Root.Path()) if path == "/" || path == "" { return Root } else { path = strings.TrimPrefix(path, "/") } return Root.Entry(path) } func getEntriesForPath(path string) []Entry { path = filepath.Clean(path) if filepath.IsAbs(path) { path = filepath.Join(Root.Path(), path) } else { path = filepath.Join(Current.Path(), path) } paths, err := filepath.Glob(path) if err != nil { panic(err) } entries := make([]Entry, 0, len(paths)) for _, path := range paths { if entry := entryByPath(path); entry != nil { entries = append(entries, entry) } } return entries } func ScanEntryLive(entry Entry, mounts map[string]struct{}) { ch := make(chan string, 10) wg := new(sync.WaitGroup) // TODO cap string size to terminal width //width, _, _ := terminal.GetSize(int(os.Stdin.Fd())) wg.Add(1) go func(ch <-chan string) { defer wg.Done() for path := range ch { os.Stdout.Write([]byte{'\r'}) clearEOL() fmt.Print(Translate("Scanning ") + path) } os.Stdout.Write([]byte{'\r'}) clearEOL() }(ch) entry.Scan(ch, mounts) close(ch) wg.Wait() } func removeMarkedEntries(entry Entry) { if entry.RemovalMark() { err := os.RemoveAll(entry.Path()) if err != nil { ColorRed.Println(Translate("Could not delete '%s': %s", entry.Path(), err.Error())) } } else { for _, entry := range entry.Entries() { removeMarkedEntries(entry) } } }