diff --git a/commands.go b/commands.go new file mode 100644 index 0000000..968a27f --- /dev/null +++ b/commands.go @@ -0,0 +1,121 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +func help(args []string) { + fmt.Println() + fmt.Println(Translate("Commands:")) + fmt.Println(Translate("ls: list files")) + fmt.Println(Translate("cd: change directory")) + fmt.Println(Translate("rm: mark files for removal")) + fmt.Println(Translate("urm: unmark files for removal")) + fmt.Println(Translate("exit: delete marked files and exit")) + fmt.Println() +} + +func ls(args []string) { + path := strings.Join(args, " ") + if path == "" { + path = "." + } + + entries := getEntriesForPath(path) + if len(entries) == 0 { + showError("'%s' could not be found", path) + return + } + + for _, entry := range entries { + fmt.Println() + fmt.Println(entry.StringRecursive(1)) + } +} + +func rm(args []string, removalMark bool) { + path := strings.Join(args, " ") + + entries := getEntriesForPath(path) + if len(entries) == 0 { + showError("'%s' could not be found", path) + return + } + + for _, entry := range entries { + entry.SetRemovalMark(removalMark) + } +} + +func cd(args []string) { + path := "/" + if len(args) > 0 { + path = args[0] + } + + entries := getEntriesForPath(path) + if len(entries) == 0 { + showError("'%s' could not be found", path) + return + } else if len(entries) > 1 { + b := new(strings.Builder) + for _, entry := range entries { + path := strings.TrimPrefix(entry.Path(), Current.Path()+"/") + path = strings.TrimPrefix(path, Root.Path()) + b.WriteString(path) + b.WriteRune('\n') + } + showError("ambiguous path: %s", path) + fmt.Println(Translate("possible paths:\n%s", b.String())) + return + } + + entry := entries[0] + if entry.IsDir() { + Current = entry + fmt.Println() + } else { + showError("'%s' is not a directory", entry.Name()) + } +} + +func exit() { + fmt.Println() + + stats := new(RemovalStats) + Root.RemovalStats(stats) + + if len(stats.Paths) == 0 { + s := bufio.NewScanner(os.Stdin) + s.Split(bufio.ScanRunes) + + fmt.Print(Translate("exit? [y/N]: ")) + if s.Scan() { + text := strings.ToLower(s.Text()) + if text == "y" || text == "yes" { + os.Exit(0) + } + } + return + } + + fmt.Println(Translate("The following items will be removed:")) + fmt.Println(stats) + + s := bufio.NewScanner(os.Stdin) + s.Split(bufio.ScanRunes) + + fmt.Print(Translate("Delete files? [y/N/c]: ")) + if s.Scan() { + text := strings.ToLower(s.Text()) + if text == "y" || text == "yes" { + removeMarkedEntries(Root) + os.Exit(0) + } else if text == "n" || text == "no" { + os.Exit(0) + } + } +} diff --git a/dir_entry.go b/dir_entry.go new file mode 100644 index 0000000..59fbcac --- /dev/null +++ b/dir_entry.go @@ -0,0 +1,183 @@ +package main + +import ( + "errors" + "os" + "path/filepath" + "sort" + "strings" +) + +type DirectoryEntry struct { + path string + size *uint64 + modSize *uint64 + entries []Entry + parent Entry + + removal bool + noPerm bool + isMount bool +} + +var _ Entry = &DirectoryEntry{} + +func (e *DirectoryEntry) Name() string { + return filepath.Base(e.path) +} + +func (e *DirectoryEntry) Path() string { + return e.path +} + +func (e *DirectoryEntry) Size() uint64 { + if e.size == nil { + size := uint64(0) + for _, c := range e.Entries() { + size += c.Size() + } + e.size = &size + + e.modSize = new(uint64) + *e.modSize = *e.size + } + + return *e.size +} + +func (e *DirectoryEntry) Entries() []Entry { + if e.entries == nil { + e.Scan(nil, nil) + } + return e.entries +} + +func (e *DirectoryEntry) String() string { + if e.removal { + return ColorRed.Sprintf("%s %s (%s)", icon("folder"), e.Name(), fmtSize(e.SizeModified())) + } + + if e.Size() == e.SizeModified() { + return ColorBlue.Sprintf("%s %s (%s)", icon("folder"), e.Name(), fmtSize(e.Size())) + } else { + return ColorBlue.Sprintf("%s %s (%s / %s)", icon("folder"), e.Name(), ColorRed.Sprintf(fmtSize(e.SizeModified())), fmtSize(e.Size())) + } +} + +func (e *DirectoryEntry) stringRecursive(depth, maxDepth int, b *strings.Builder) { + if depth > maxDepth { + return + } + + b.WriteString(strings.Repeat(" ", depth)) + b.WriteString(e.String()) + b.WriteRune('\n') + + for _, entry := range e.Entries() { + entry.stringRecursive(depth+1, maxDepth, b) + } +} + +func (e *DirectoryEntry) StringRecursive(depth int) string { + b := new(strings.Builder) + e.stringRecursive(0, depth, b) + return b.String() +} + +func (e *DirectoryEntry) Scan(ch chan<- string, mounts map[string]struct{}) { + dirEntries, err := os.ReadDir(e.Path()) + if errors.Is(err, os.ErrPermission) { + e.noPerm = true + e.entries = []Entry{} + return + } else if err != nil { + e.noPerm = true + e.entries = []Entry{} + return + } + + e.entries = make([]Entry, 0, len(dirEntries)) + for _, dirEntry := range dirEntries { + entry := NewEntryFromDirEntry(e, dirEntry) + if _, isMount := mounts[entry.Path()]; !isMount { + e.entries = append(e.entries, entry) + entry.Scan(ch, mounts) + } + } + + if ch != nil { + ch <- e.Path() + } + sort.Slice(e.entries, func(i, j int) bool { + return e.entries[i].SizeModified() < e.entries[j].SizeModified() + }) +} + +func (e *DirectoryEntry) Parent() Entry { + return e.parent +} + +func (e *DirectoryEntry) Entry(path string) Entry { + if path == "." || path == "" { + return e + } + + splits := strings.Split(path, "/") + for _, entry := range e.Entries() { + if entry.Name() == splits[0] { + return entry.Entry(strings.Join(splits[1:], "/")) + } + } + + return nil +} + +func (e *DirectoryEntry) IsDir() bool { + return true +} + +func (e *DirectoryEntry) SetRemovalMark(mark bool) { + e.removal = mark + for _, entry := range e.Entries() { + entry.SetRemovalMark(mark) + } + + if e.Parent() != nil { + pentries := e.Parent().Entries() + sort.Slice(pentries, func(i, j int) bool { + return pentries[i].SizeModified() < pentries[j].SizeModified() + }) + } +} + +func (e *DirectoryEntry) RemovalMark() bool { + return e.removal +} + +func (e *DirectoryEntry) SizeModified() uint64 { + if e.size == nil { + e.Size() + } + + return *e.modSize +} + +func (e *DirectoryEntry) modifySize(modifier uint64) { + *e.modSize += modifier + if e.parent != nil { + e.parent.modifySize(modifier) + } +} + +func (e *DirectoryEntry) RemovalStats(stats *RemovalStats) { + if e.RemovalMark() { + stats.Dirs++ + if e.Parent() != nil && !e.Parent().RemovalMark() { + stats.Paths = append(stats.Paths, e.Path()) + } + } + + for _, entry := range e.Entries() { + entry.RemovalStats(stats) + } +} diff --git a/diskspace b/diskspace new file mode 100755 index 0000000..f9c2691 Binary files /dev/null and b/diskspace differ diff --git a/entry.go b/entry.go new file mode 100644 index 0000000..88a9391 --- /dev/null +++ b/entry.go @@ -0,0 +1,72 @@ +package main + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" +) + +type Entry interface { + fmt.Stringer + stringRecursive(depth, maxDepth int, b *strings.Builder) + StringRecursive(depth int) string + + Name() string + Path() string + Size() uint64 + SizeModified() uint64 + Entries() []Entry + Entry(path string) Entry + + SetRemovalMark(mark bool) + RemovalMark() bool + RemovalStats(stats *RemovalStats) + + modifySize(modifier uint64) + + IsDir() bool + + Parent() Entry + + Scan(ch chan<- string, mounts map[string]struct{}) +} + +func NewEntryFromDirEntry(parentEntry Entry, dirEntry fs.DirEntry) Entry { + if dirEntry.IsDir() { + return &DirectoryEntry{ + path: filepath.Join(parentEntry.Path(), dirEntry.Name()), + parent: parentEntry, + } + } else { + size := uint64(0) + noPerm := false + if fi, err := dirEntry.Info(); err == nil { + size = uint64(fi.Size()) + } else { + noPerm = true + } + return &FileEntry{ + path: filepath.Join(parentEntry.Path(), dirEntry.Name()), + size: &size, + noPerm: noPerm, + parent: parentEntry, + } + } +} + +func NewEntry(path string) (Entry, error) { + path = filepath.Clean(path) + + fi, err := os.Lstat(path) + if err != nil { + return nil, err + } + + if fi.IsDir() { + return &DirectoryEntry{path: path}, nil + } else { + return &FileEntry{path: path}, nil + } +} diff --git a/file_entry.go b/file_entry.go new file mode 100644 index 0000000..b749b53 --- /dev/null +++ b/file_entry.go @@ -0,0 +1,149 @@ +package main + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" +) + +type FileEntry struct { + path string + size *uint64 + modSize *uint64 + parent Entry + + removal bool + noPerm bool + isMount bool + isExec bool +} + +var _ Entry = &FileEntry{} + +func (e *FileEntry) Name() string { + return filepath.Base(e.path) +} + +func (e *FileEntry) Path() string { + return e.path +} + +func (e *FileEntry) Size() uint64 { + if e.size == nil { + e.Scan(nil, nil) + } + + return *e.size +} + +func (e *FileEntry) Entries() []Entry { + return []Entry{} +} + +func (e *FileEntry) String() string { + var ic string + if e.isExec { + ic = icon("app") + } else { + ic = icon(filepath.Ext(e.Path())) + } + + if e.removal { + return ColorRed.Sprintf( + "%s %s (%s / %s)", + ic, e.Name(), fmtSize(e.SizeModified()), fmtSize(e.Size()), + ) + } + + if e.isExec { + return ColorGreen.Sprintf("%s %s (%s)", ic, e.Name(), fmtSize(e.Size())) + } + + return fmt.Sprintf("%s %s (%s)", ic, e.Name(), fmtSize(e.Size())) +} + +func (e *FileEntry) stringRecursive(depth, maxDepth int, b *strings.Builder) { + if depth > maxDepth { + return + } + + b.WriteString(strings.Repeat(" ", depth)) + b.WriteString(e.String()) + b.WriteRune('\n') +} + +func (e *FileEntry) StringRecursive(depth int) string { + b := new(strings.Builder) + e.stringRecursive(0, depth, b) + return b.String() +} + +func (e *FileEntry) Scan(ch chan<- string, mounts map[string]struct{}) { + if ch != nil { + ch <- e.Path() + } + + s := uint64(0) + if fi, err := os.Lstat(e.Path()); err == nil { + s = uint64(fi.Size()) + e.isExec = fi.Mode()&0x41 > 0 // 0x41 == 0b001000001 == rwXrwxrwX + } else if errors.Is(err, os.ErrPermission) { + e.noPerm = true + } + e.size = &s + + e.modSize = new(uint64) + *e.modSize = *e.size +} + +func (e *FileEntry) Parent() Entry { + return e.parent +} + +func (e *FileEntry) Entry(path string) Entry { + return e +} + +func (e *FileEntry) IsDir() bool { + return false +} + +func (e *FileEntry) SetRemovalMark(mark bool) { + e.removal = mark + if mark { + e.modifySize(-*e.size) + } else { + e.modifySize(*e.size) + } +} + +func (e *FileEntry) RemovalMark() bool { + return e.removal +} + +func (e *FileEntry) SizeModified() uint64 { + if e.size == nil { + e.Size() + } + + return *e.modSize +} + +func (e *FileEntry) modifySize(modifier uint64) { + *e.modSize += modifier + if e.parent != nil { + e.parent.modifySize(modifier) + } +} + +func (e *FileEntry) RemovalStats(stats *RemovalStats) { + if e.RemovalMark() { + stats.Files++ + stats.Bytes += *e.size + if e.Parent() != nil && !e.Parent().RemovalMark() { + stats.Paths = append(stats.Paths, e.Path()) + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1a794e3 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module git.tordarus.net/Tordarus/diskspace + +go 1.18 + +require ( + github.com/fatih/color v1.13.0 + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d +) + +require ( + github.com/mattn/go-colorable v0.1.9 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..01052f0 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/main.go b/main.go new file mode 100644 index 0000000..6209a62 --- /dev/null +++ b/main.go @@ -0,0 +1,185 @@ +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)") +) + +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) + } + } +} diff --git a/mount.go b/mount.go new file mode 100644 index 0000000..043dbdd --- /dev/null +++ b/mount.go @@ -0,0 +1,32 @@ +package main + +import ( + "bufio" + "bytes" + "os/exec" + "regexp" +) + +var ( + mountLineRegex = regexp.MustCompile(`^.*? on (.*?) type (.*?) \((?:.*?[,)])*$`) +) + +func Mounts() (map[string]struct{}, error) { + mounts := map[string]struct{}{} + + out, err := exec.Command("mount").CombinedOutput() + if err != nil { + return nil, err + } + + s := bufio.NewScanner(bytes.NewReader(out)) + for s.Scan() { + matches := mountLineRegex.FindAllStringSubmatch(s.Text(), -1) + if len(matches) == 0 { + continue + } + mounts[matches[0][1]] = struct{}{} + } + + return mounts, nil +} diff --git a/stats.go b/stats.go new file mode 100644 index 0000000..db606a2 --- /dev/null +++ b/stats.go @@ -0,0 +1,34 @@ +package main + +import ( + "strings" +) + +type RemovalStats struct { + Files int + Dirs int + Bytes uint64 + Paths []string +} + +func (s *RemovalStats) String() string { + b := new(strings.Builder) + + for _, path := range s.Paths { + b.WriteString(path) + b.WriteRune('\n') + } + + b.WriteRune('\n') + + b.WriteString(Translate("Files: %d", s.Files)) + b.WriteRune('\n') + + b.WriteString(Translate("Directories: %d", s.Dirs)) + b.WriteRune('\n') + + b.WriteString(Translate("Total size: %s", fmtSize(s.Bytes))) + b.WriteRune('\n') + + return b.String() +} diff --git a/translate.go b/translate.go new file mode 100644 index 0000000..cc4d469 --- /dev/null +++ b/translate.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "os" + "strings" +) + +var Languages = map[string]map[string]string{ + "de": map[string]string{ + "Scanning ": "Verarbeite ", + "Scanning took %s": "Verarbeitung dauerte %s", + "'%s' is not a directory": "'%s' ist kein Verzeichnis", + "The following items will be removed:": "Die folgenden Elemente werden entfernt:", + "Files: %d": "Dateien: %d", + "Directories: %d": "Ordner: %d", + "Total size: %s": "Gesamtgröße: %s", + "'%s' could not be found": "'%s' konnte nicht gefunden werden", + "Could not delete '%s': %s": "'%s' konnte nicht gelöscht werden: %s", + "Delete files? [y/N/c]: ": "Dateien löschen? [y/N/c]: ", + "ambiguous path: %s": "Nicht eindeutiger Pfad: %s", + "possible paths:\n%s": "Mögliche Pfade:\n%s", + "Commands:": "Befehle:", + "ls: list files": "ls: Dateien auflisten", + "cd: change directory": "cd: Verzeichnis wechseln", + "rm: mark files for removal": "rm: Markiere Dateien zur Löschung", + "urm: unmark files for removal": "urm: Entmarkiere Dateien zur Löschung", + "exit: delete marked files and exit": "exit: Lösche markierte Dateien und beende Programm", + "Use command 'exit' for exiting properly": "Nutze den Befehl 'exit', um das Programm ordnungsgemäß zu beenden", + "exit? [y/N]: ": "Beenden? [y/N]: ", + }, + "ja": map[string]string{ + "Scanning ": "処理 ", + "Scanning took %s": "処理が %s かかりました", + "'%s' is not a directory": "「%s」はディレクトリではありません", + "The following items will be removed:": "このファイルは削除します:", + "Files: %d": "ファイル: %d", + "Directories: %d": "デイレクトリ: %d", + "Total size: %s": "全額: %s", + "'%s' could not be found": "「%s」が見つかりませんでした", + "Could not delete '%s': %s": "「%s」が削除できませんでした: %s", + "Delete files? [y/N/c]: ": "削除してほしいですか? [y/N/c]: ", + "ambiguous path: %s": "あいまいなパス: %s", + "possible paths:\n%s": "可能なパス:\n%s", + "Commands:": "コマンド:", + "ls: list files": "ls: ファイルを並べます", + "cd: change directory": "cd: ディレクトリを変更", + "rm: mark files for removal": "rm: ファイルに削除のマークを付けます", + "urm: unmark files for removal": "urm: ファイルに削除のマークを除きます", + "exit: delete marked files and exit": "exit: 削除のマークされたファイルを削除して終了", + "Use command 'exit' for exiting properly": "「exit」コマンドを使って終了します", + "exit? [y/N]: ": "終了してほしいですか? [y/N]: ", + }, +} + +var langmap map[string]string + +func Translate(str string, args ...interface{}) string { + if langmap == nil { + langVar, _ := os.LookupEnv("LANG") + langmap = map[string]string{} + for lname, lmap := range Languages { + if strings.HasPrefix(langVar, lname) { + langmap = lmap + } + } + } + + if translatedText, ok := langmap[str]; ok { + return fmt.Sprintf(translatedText, args...) + } + return fmt.Sprintf(str, args...) +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..ec48361 --- /dev/null +++ b/utils.go @@ -0,0 +1,77 @@ +package main + +import ( + "math" + "os" + "strconv" +) + +func fmtSize(size uint64) string { + sf := float64(size) + if size/1000000000000 >= 1 { + return strconv.FormatFloat(round(sf/1000000000000), 'G', -1, 64) + " TB" + } else if size/1000000000 >= 1 { + return strconv.FormatFloat(round(sf/1000000000), 'G', -1, 64) + " GB" + } else if size/1000000 >= 1 { + return strconv.FormatFloat(round(sf/1000000), 'G', -1, 64) + " MB" + } else if size/1000 >= 1 { + return strconv.FormatFloat(round(sf/1000), 'G', -1, 64) + " KB" + } else { + return strconv.FormatFloat(round(sf), 'G', -1, 64) + " B" + } +} + +func round(value float64) float64 { + return math.Round(value*100) / 100 +} + +func icon(ext string) string { + switch *IconTheme { + default: + fallthrough + case "unicode": + switch ext { + case "folder": + return "📂" + default: + return "📄" + } + case "nerd": + switch ext { + case "app": + return "ﬓ" + case "folder": + return "" + case ".doc", ".docx", ".odt", ".rtx", ".pdf", ".tex": + return "" + case ".xls", ".xlsx", ".xlsm", ".dex", ".ods", ".csv": + return "" + case ".jpg", ".jpeg", ".png", ".gif", ".ppm", ".svg", ".tiff", ".bmp", ".webp", ".blend", ".obj": + return "" + case ".go", ".c", ".cpp", ".rs", ".h", ".java", ".asm", ".js", ".html", ".css", ".ts", ".sql": + return "" + case ".mp3", ".flac", ".m4a", ".opus", ".wav", ".wma": + return "" + case ".mp4", ".webm", ".mkv", ".flv", ".ogg", ".ogv", ".gifv", ".avi", ".mov", ".wmv", ".mp2", ".mpg", ".mpv", ".mpe", ".mpeg", ".m2v", ".3gp": + return "" + case ".txt": + return "" + case ".zip", ".rar", ".7z", ".tar", ".bz2", ".gz", ".xz", ".tgz", ".tbz2", ".txz": + return "" + case ".iso": + return "﫭" + case ".apk": + return "" + case ".jar": + return "" + case ".json", ".yaml", ".yml": + return "" + default: + return "" + } + } +} + +func clearEOL() { + os.Stdout.Write([]byte{0x1b, 0x5b, 0x4b}) +}