├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── arguments.go ├── arguments_test.go ├── file_types.go ├── fileinfo.go ├── filelist.go ├── fmt.go ├── investigate.go ├── main.go ├── paths.go ├── sort.go ├── traverse.go ├── trie.go ├── tty_unix.go ├── tty_win.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | example-dir # the dir I will use for testing -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - tip 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Welcome! Here be a general repo overview to serve as an intro to a young lady or gentleman who is considering reading up on they things are set up around here and possibly contributing. 2 | 3 | Conventions 4 | ----- 5 | Everything is `go fmt` and `go lint` compliant. 6 | All the major editors support plugins that enable doing this on the fly, they are very helpful. 7 | 8 | Another useful tool that I found helpful is `goimports`. 9 | 10 | At the moment I want to move to maximum modularity, so that each separate functionality domain is moved into its own separate package. 11 | I have heard of people arguing that it is not sustainable but not yet convinced. Maybe this will fail and that would be my way to grok their argument. 12 | 13 | Intro Walkthrough & Concepts 14 | ----- 15 | ###Mode 16 | 17 | `mode *Mode`, defined in [arguments.go](arguments.go), stores all things involving the settings of the running instance: 18 | ```go 19 | type Mode struct { 20 | inputPath string // target path literal from the argument parsing (e.g. "~/hi") 21 | absolutePath string // absolute path to the target directory (e.g. "/home/dima/hi") 22 | // ... 23 | // and ... boolean flags for specific modes 24 | } 25 | ``` 26 | We have target directory's path here and all the mode flags. 27 | 28 | `ParseArguments()` parses the arguments passed on to `lsp` into mode flags and target dir. There are many excellent libraries that parse flag arguments, I ended up rewriting my own to enable `ls`-style single letter triggers within one flag (so that `lsp -al` is equivalent to `lsp -a -l` or `lsp -la`). 29 | 30 | 31 | Breakdown of Contents 32 | ----- 33 | See [godocs.org](http://godoc.org/github.com/dborzov/lsp): 34 | * `lsp.go` contains main() function 35 | * `arguments.go` parses arguments in order inferring the target path and flags invoked to the best of its ability. 36 | * `fileinfo.go` contains FileInfo struct with what is known of individual file/folder in the list and methods to fetch this info 37 | * `filelist.go` contains Filelist []Fileinfo definition and methods to do the two tasks: filter, sort and represent as appropriate to the running mode (flags) 38 | * `fmt.go`, complements render.go, containing stuff to format output in the stdoutt/terminal, but fmt.go is for more bash-specific/lower level stuff 39 | * `investigate.go` contains functions to "investigate" individual files for type, size, binary/text directories for character of their content and so on 40 | * `paths.go` contains filesystem path parsing and manipulation operations 41 | * `sort.go` contains mode-specific file sorting interface implementations 42 | * `traverse.go`: traverse group Trie and select representation grouping 43 | * `trie.go` contains a Trie of grouped/classified files with attributes as nodes. Root node has 3 parents: directories, reg. files and special files (everything else, like symlinks, devices and so on) it goes down up to file extension, modification time and so on. 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Peter Borzov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## lsp: list files in a mildly human-frendlier manner 2 | [![Build Status](https://travis-ci.org/dborzov/lsp.svg?branch=master)](https://travis-ci.org/dborzov/lsp) 3 | 4 | `lsp` lists files, like [`ls`](http://en.wikipedia.org/wiki/Ls) command, 5 | but it does not attempt to meet 6 | that archaic POSIX specification, so instead of this: 7 | ``` 8 | (bash)$ ls -l 9 | 10 | total 16 11 | -rw-r--r-- 1 peterborzov staff 1079 9 Aug 00:22 LICENSE 12 | -rw-r--r-- 1 peterborzov staff 60 9 Aug 00:22 README.md 13 | ``` 14 | 15 | you get this: 16 | ![screenshot](https://raw.githubusercontent.com/dborzov/lsp/screenshots/symlinks.png) 17 | 18 | ## Features 19 | #### File Groups 20 | Files grouped by type (with `-l` key or in modes when file type not shown). `lsp` distinguishes binary, text and executable files, symlinks and is aware of weird types like devices and unix socket thingy: 21 | ![lsp can show files grouped by type](https://raw.githubusercontent.com/dborzov/lsp/screenshots/grouped.png) 22 | #### Modification time in human-friendly format 23 | `-t` key for when you are interested in modification time. It turns to the mode that makes most sense to me when I want to look up modtimes, sorted within file groups from recent to latest: 24 | ![](https://raw.githubusercontent.com/dborzov/lsp/screenshots/modtime.png) 25 | Sometimes relative times are not very readible as well (like when you are interested in a specific date), use two flags `-sl` to show the full UTC timestamp in properties. 26 | #### Size in human-friendly format 27 | `-s` key, similarly to modtime key, shows file sizes and sorts within file groups from largest to smallest: 28 | ![](https://raw.githubusercontent.com/dborzov/lsp/screenshots/size.png) 29 | 30 | #### Async Timeout 31 | The file information is collected asynchronously, BFS-like, with a separate thread for each file and a timeout threshold. 32 | 33 | That means that the execution is not going to freeze because of some low-response device driver (like external hard drive or optical drive) or collecting info about a huge directory. 34 | 35 | #### Align by left 36 | I have been playing with aligning files and descriptions by center, and I like that you can see files with the same extension right away, but there are deifinitely cases when it gets weird. 37 | For now, there is `-p` key to render the file table in the left-aligned columns: 38 | ![](https://raw.githubusercontent.com/dborzov/lsp/screenshots/table.png) 39 | 40 | 41 | ## Todo before v1.0 42 | - [ ] Rewrite outline formatting: with the current design too much space is wasted, long filenames break things 43 | - [x] Mark executable files as such 44 | - Think about how to represent file rights and ownership 45 | - Approach hidden and generated files as outlined in [issue#3](https://github.com/dborzov/lsp/issues/3) 46 | - Better test coverage 47 | - Expand in this README on philosophy of the project (tool in the unix way, minimize surprises, nothing's to be configurable) 48 | - Think of TODO list points 49 | 50 | Github Issues and pull requests are very welcome, feel free to [message me](tihoutrom@gmail.com) if you are considering contributing. 51 | See [CONTRIBUTING.md](CONTRIBUTING.md) for intro to the codebase 52 | 53 | 54 | ## Installation 55 | 56 | `lsp` is written in the `go` programming language. 57 | It can be installed using `go get`. 58 | 59 | ``` 60 | $ go get github.com/dborzov/lsp 61 | ``` 62 | 63 | Then make sure that your `$PATH` includes the `$GOPATH/bin` directory. 64 | To do that, you can put this line your `~/.bash_profile` or `.zshrc`: 65 | ``` 66 | export PATH=$PATH:$GOPATH/bin 67 | ``` 68 | 69 | Once it becomes more functional, `lsp` will be distributed in native binaries 70 | (without dependencies) for all platforms (Linux, MacOS, Windows). 71 | 72 | ## Misc 73 | MIT license. 74 | -------------------------------------------------------------------------------- /arguments.go: -------------------------------------------------------------------------------- 1 | // arguments.go parses arguments in order inferring 2 | // the target path and flags invoked to the best of its ability. 3 | // 4 | package main 5 | 6 | import ( 7 | filepath "path/filepath" 8 | ) 9 | 10 | var mode *Mode 11 | 12 | // Mode reflects running mode with superset of ls flags and target path 13 | type Mode struct { 14 | inputPath string // target path literal from the argument parsing (e.g. "~/hi") 15 | absolutePath string // absolute path to the target directory (e.g. "/home/dima/hi") 16 | comments []string // free form descriptions of dir in question (like when it is a git repo) 17 | 18 | summary bool // no header for file group, file type in desscription column 19 | d bool // shows directories only 20 | h bool // "himan-readable" mode 21 | long bool // "long" form, more details 22 | size bool // "show and order by size" mode 23 | time bool // "show and order by modification time" mode 24 | pyramid bool // align files to the center or to the sides 25 | } 26 | 27 | const flagDash = '-' 28 | 29 | var err error 30 | 31 | // ParseArguments parses the arguments passed on to `lsp` into 32 | // mode flags and target dir. 33 | // There are many excellent libraries that parse flag arguments, 34 | // but I ended up rewriting my own to enable 35 | // `ls`-style single letter triggers within one flag 36 | // (so that `lsp -al` is equivalent to `lsp -a -l` or `lsp -la`) 37 | func ParseArguments(arguments []string) (*Mode, error) { 38 | mode = new(Mode) 39 | for _, l := range arguments[1:] { 40 | if l[0] == flagDash { 41 | // this argument seems to be a flag 42 | for _, flag := range l[1:] { 43 | var f *bool 44 | switch flag { 45 | case 'd': 46 | f = &mode.d 47 | case 'h': 48 | f = &mode.h 49 | case 'l': 50 | f = &mode.long 51 | case 's': 52 | f = &mode.size 53 | case 'p': 54 | f = &mode.pyramid 55 | case 't': 56 | f = &mode.time 57 | } 58 | if f != nil { 59 | *f = true 60 | } 61 | } 62 | } else { 63 | mode.inputPath = mode.inputPath + l 64 | } 65 | } 66 | 67 | mode.summary = !(mode.time || mode.size || mode.long) 68 | 69 | if mode.inputPath == "" { 70 | mode.inputPath = "." 71 | } 72 | 73 | mode.absolutePath, err = filepath.Abs(mode.inputPath) 74 | return mode, err 75 | } 76 | -------------------------------------------------------------------------------- /arguments_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestParseArguments(t *testing.T) { 6 | in := []string{"lsp", "-a"} 7 | 8 | x, err := ParseArguments(in) 9 | 10 | if err != nil { 11 | t.Errorf("ParseAguments(%#v) = %#v, %#v ", in, x, err) 12 | return 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /file_types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "strings" 4 | 5 | var presetTitles = map[string]string{ 6 | "dirs": "Directories", 7 | "regulars": "Regular files", 8 | "specials": "Special Files (Neither Dirs Nor Regulars)", 9 | "regulars>text": "Text Files", 10 | "regulars>executables": "Executables", 11 | "regulars>blobs": "Blobs", 12 | "regulars>empty": "Empty Files", 13 | "special>device": "Devices", 14 | "special>symlink": "Symlinks", 15 | "special>unix domain socket": "UNIX Domain Socket", 16 | } 17 | 18 | func nameTriePath(path []string) string { 19 | grp := strings.Join(path, ">") 20 | if title, ok := presetTitles[grp]; ok { 21 | return title 22 | } 23 | return grp 24 | } 25 | -------------------------------------------------------------------------------- /fileinfo.go: -------------------------------------------------------------------------------- 1 | // fileinfo.go contains FileInfo struct with what is known 2 | // of individual file/folder in the list and methods to fetch this info 3 | package main 4 | 5 | import ( 6 | "os" 7 | 8 | humanize "github.com/dustin/go-humanize" 9 | ) 10 | 11 | // FileInfo is to store everything known about the file object 12 | type FileInfo struct { 13 | f os.FileInfo 14 | special string // description for symlinks, device files and named pipes or unix domain sockets, empty otherwise 15 | description string 16 | hidden bool 17 | } 18 | 19 | // Description yeilds description line appropriate to the running mode 20 | func (fi FileInfo) Description() (description string) { 21 | switch { 22 | case mode.size: 23 | description = fi.representSize() 24 | case mode.time && mode.long: 25 | description = fi.representTimeDetailed() 26 | case mode.time: 27 | description = fi.representTime() 28 | case mode.summary: 29 | description = fi.special 30 | if fi.description != "" { 31 | description += "[DEFAULT](" + fi.description + ")" 32 | } 33 | default: 34 | description = fi.description 35 | 36 | } 37 | return 38 | } 39 | 40 | func (fi FileInfo) representSize() string { 41 | if fi.f.IsDir() { 42 | return fi.description 43 | } 44 | return humanize.Bytes(uint64(fi.f.Size())) 45 | } 46 | 47 | func (fi FileInfo) representTimeDetailed() string { 48 | return humanize.Time(fi.f.ModTime()) + " (" + fi.f.ModTime().String() + ")" 49 | } 50 | 51 | func (fi FileInfo) representTime() string { 52 | return humanize.Time(fi.f.ModTime()) 53 | } 54 | -------------------------------------------------------------------------------- /filelist.go: -------------------------------------------------------------------------------- 1 | // filelist.go contains Filelist []Fileinfo definition 2 | // and methods to do the two tasks: filter, sort and 3 | // represent as appropriate to the running mode (flags) 4 | package main 5 | 6 | import "os" 7 | 8 | // FileList maintains current file items to show 9 | var FileList []FileInfo 10 | 11 | // FileListUpdate typed channels contain results of Fileinfo elements in FileList resolved asynchroniously 12 | type FileListUpdate struct { 13 | i int // index of update element 14 | item *FileInfo 15 | done bool // don't wait for more updates 16 | } 17 | 18 | func researchFileList(files []os.FileInfo) []FileInfo { 19 | fileList := make([]FileInfo, len(files)) 20 | results := make(chan FileListUpdate) 21 | for i, f := range files { 22 | fileList[i].f = f 23 | go fileList[i].InvestigateFile(i, results) 24 | } 25 | 26 | setTimeoutTimer() 27 | 28 | leftNotUpdated := len(files) 29 | 30 | for leftNotUpdated > 0 { 31 | select { 32 | case u := <-results: 33 | if u.done { 34 | leftNotUpdated-- 35 | } 36 | if u.item != nil { 37 | fileList[u.i] = *u.item 38 | } 39 | case <-timeout: 40 | leftNotUpdated = 0 41 | } 42 | } 43 | return fileList 44 | } 45 | -------------------------------------------------------------------------------- /fmt.go: -------------------------------------------------------------------------------- 1 | // fmt.go for sutff to format output in the stdoutt/terminal 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "sort" 8 | "strings" 9 | "unicode/utf8" 10 | 11 | isatty "github.com/mattn/go-isatty" 12 | c "github.com/mitchellh/colorstring" 13 | ) 14 | 15 | const ( 16 | dashesNumber = 2 17 | ) 18 | 19 | var ( 20 | terminalWidth = 80 21 | columnSize = 39 // characters in the filename column 22 | ) 23 | 24 | // Defines Terminal Coloring Theme 25 | var ColorScheme c.Colorize 26 | 27 | // BlankScheme color scheme just deletes all the coloring tags 28 | // this colorScheme is applied before fetching the string length 29 | // so that color tags (or color decoding escape symbols ) 30 | // do not screw up column alignment and so on 31 | var BlankScheme c.Colorize 32 | 33 | func init() { 34 | ColorScheme = c.Colorize{ 35 | Colors: map[string]string{ 36 | "DEFAULT": c.DefaultColors["default"], 37 | "FILENAME": c.DefaultColors["light_green"], 38 | "META": c.DefaultColors["red"], 39 | "DESCRIPTION": c.DefaultColors["light_yellow"], 40 | "HR": c.DefaultColors["light_cyan"], 41 | "NUMBER": c.DefaultColors["light_red"], 42 | }, 43 | Reset: true, 44 | Disable: !isatty.IsTerminal(os.Stdout.Fd()), 45 | } 46 | 47 | BlankScheme = ColorScheme 48 | BlankScheme.Disable = true 49 | 50 | } 51 | 52 | func render() { 53 | SetColumnSize() 54 | Traverse() 55 | renderSummary() 56 | } 57 | 58 | func renderSummary() { 59 | printHR() 60 | printCentered(fmt.Sprintf(ColorScheme.Color("[DEFAULT]lsp \"[NUMBER]%s[DEFAULT]\""), presentPath(mode.absolutePath)) + fmt.Sprintf(ColorScheme.Color(", [NUMBER]%v[DEFAULT] files, [NUMBER]%v[DEFAULT] directories"), len(FileList), len(Trie.Ch["dirs"].Fls))) 61 | for _, cm := range mode.comments { 62 | printCentered(cm) 63 | } 64 | } 65 | 66 | func renderFiles(fls []*FileInfo) { 67 | switch { 68 | case mode.size: 69 | sort.Sort(sizeSort(fls)) 70 | case mode.time: 71 | sort.Sort(timeSort(fls)) 72 | default: 73 | sort.Sort(alphabeticSort(fls)) 74 | } 75 | for _, fl := range fls { 76 | if !fl.hidden { 77 | PrintColumns(fl.f.Name(), fl.Description()) 78 | } 79 | } 80 | } 81 | 82 | // PrintColumns prints two-column table row, nicely formatted and shortened if needed 83 | func PrintColumns(filename, description string) { 84 | maxFileNameSize := columnSize - 6 85 | if utf8.RuneCountInString(filename) > maxFileNameSize { 86 | filename = string([]rune(filename)[0:maxFileNameSize]) + "[META][...]" 87 | } 88 | 89 | indentSize := columnSize - utf8.RuneCountInString(BlankScheme.Color(filename)) 90 | 91 | if !mode.pyramid { 92 | fmt.Printf(ColorScheme.Color(fmt.Sprintf("[FILENAME]%s", filename))) 93 | fmt.Printf(strings.Repeat(" ", indentSize)) 94 | } else { 95 | fmt.Printf(strings.Repeat(" ", indentSize)) 96 | fmt.Printf(ColorScheme.Color(fmt.Sprintf("[FILENAME]%s", filename))) 97 | } 98 | // central dividing space 99 | fmt.Printf(" ") 100 | fmt.Printf(ColorScheme.Color(fmt.Sprintf("[DESCRIPTION]%s\n", description))) 101 | } 102 | 103 | func printHeader(o string) { 104 | length := utf8.RuneCountInString(o) 105 | sideburns := (6+2*columnSize-length)/2 - dashesNumber 106 | if sideburns < 0 { 107 | sideburns = 0 108 | } 109 | fmt.Printf(strings.Repeat(" ", sideburns)) 110 | fmt.Printf(ColorScheme.Color("[DESCRIPTION]" + strings.Repeat("-", dashesNumber) + o + strings.Repeat("-", dashesNumber) + "[DEFAULT]\n")) 111 | } 112 | 113 | func printCentered(o string) { 114 | length := utf8.RuneCountInString(o) 115 | sideburns := (6 + 2*columnSize - length) / 2 116 | if sideburns < 0 { 117 | sideburns = 0 118 | } 119 | fmt.Printf(strings.Repeat(" ", sideburns)) 120 | fmt.Printf(ColorScheme.Color(o + "\n")) 121 | } 122 | 123 | func printHR() { 124 | fmt.Printf(ColorScheme.Color("[HR]" + strings.Repeat("-", terminalWidth) + "\n")) 125 | } 126 | -------------------------------------------------------------------------------- /investigate.go: -------------------------------------------------------------------------------- 1 | // investigate.go contains functions to "investigate" individual files for type, size, binary/text 2 | // directories for character of their content and so on 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | humanize "github.com/dustin/go-humanize" 13 | ) 14 | 15 | const ( 16 | numberOfReadTestBytes = 1024 17 | ) 18 | 19 | // InvestigateFile prepares detailed file/directory summary 20 | func (fi FileInfo) InvestigateFile(i int, updated chan FileListUpdate) { 21 | m := fi.f.Mode() 22 | done := true 23 | switch { 24 | case m&os.ModeSymlink != 0: 25 | fi.special = "symlink" 26 | link, err := filepath.EvalSymlinks(mode.absolutePath + "/" + fi.f.Name()) 27 | if err == nil { 28 | // will eventually use strings.TrimPrefix to shorten for things like homepath 29 | fi.description = "link: [FILENAME]" + presentPath(link) + "[DEFAULT]" 30 | } else { 31 | fi.description = "got an error trying to resolve symlink" 32 | } 33 | case m&os.ModeDevice != 0: 34 | fi.special = "device" 35 | case m&os.ModeNamedPipe != 0: 36 | fi.special = "unix named pipe" 37 | case m&os.ModeSocket != 0: 38 | fi.special = "unix domain socket" 39 | case m&os.ModeAppend != 0: 40 | fi.special = "append-only file" 41 | case m&os.ModeExclusive != 0: 42 | fi.special = "exclusive-use file" 43 | case fi.f.Name() == ".git": 44 | fi.hidden = true 45 | remote := investigateGit(mode.absolutePath) 46 | if remote != "" { 47 | mode.comments = append(mode.comments, "git repo (remote at "+remote+")") 48 | } else { 49 | mode.comments = append(mode.comments, "git repo") 50 | } 51 | case m&os.ModeDir != 0: 52 | fi.special = "dir" 53 | go fi.investigateDir(i, updated) 54 | done = false 55 | case m&0111 != 0: 56 | fi.special = "Executable" 57 | default: 58 | fi.special = "regular" 59 | go fi.investigateRegFile(i, updated) 60 | done = false 61 | } 62 | 63 | updated <- FileListUpdate{i, &fi, done} 64 | } 65 | 66 | func (fi FileInfo) investigateRegFile(i int, updated chan FileListUpdate) { 67 | if fi.f.Size() == 0 { 68 | fi.description = "Empty File" 69 | fi.special = "Empty File" 70 | updated <- FileListUpdate{i, &fi, true} 71 | return 72 | } 73 | isTxt, err := CheckIfTextFile(fi) 74 | if err != nil { 75 | updated <- FileListUpdate{i, nil, true} 76 | return 77 | } 78 | if isTxt { 79 | fi.special = "Text File" 80 | } else { 81 | fi.special = "Binary File" 82 | } 83 | updated <- FileListUpdate{i, &fi, true} 84 | } 85 | 86 | func (fi FileInfo) investigateDir(i int, updated chan FileListUpdate) { 87 | path := filepath.Join(mode.absolutePath, fi.f.Name()) 88 | 89 | fileCount, fileSize := getNumberOfFilesInDir(path) 90 | 91 | fi.description = fmt.Sprintf(ColorScheme.Color("[FILENAME]%d[DEFAULT] files; [FILENAME]%s[DEFAULT]"), fileCount, humanize.Bytes(uint64(fileSize))) 92 | 93 | if fileCount == -1 { 94 | fi.description = fmt.Sprintf(ColorScheme.Color("can't read its content")) 95 | } 96 | 97 | if fileCount == 0 { 98 | fi.description = fmt.Sprintf(ColorScheme.Color("empty one")) 99 | } 100 | 101 | if fileCount == 1 { 102 | fi.description = fmt.Sprintf(ColorScheme.Color("just one file")) 103 | } 104 | 105 | isgit := investigateGit(path) 106 | if isgit != "" { 107 | fi.description = isgit 108 | } 109 | updated <- FileListUpdate{i, &fi, true} 110 | } 111 | 112 | func getNumberOfFilesInDir(path string) (count int, size int) { 113 | files, err := ioutil.ReadDir(path) 114 | if err != nil { 115 | return -1, -1 116 | } 117 | 118 | for _, f := range files { 119 | if f.IsDir() { 120 | c, s := getNumberOfFilesInDir(path + "/" + f.Name()) 121 | count += c 122 | size += s 123 | if c == -1 { 124 | return -1, -1 125 | } 126 | } else { 127 | count += 1 128 | size += int(f.Size()) 129 | } 130 | } 131 | return 132 | } 133 | 134 | // CheckIfTextFile tests if the file is text or binary 135 | // using the bash's diff tool method: 136 | // by reading the first numberOfReadTestBytes bytes 137 | // and looking for NULL byte. If there is one encountered, 138 | // it is probably a binary. 139 | func CheckIfTextFile(file FileInfo) (bool, error) { 140 | var bytesToRead int64 = numberOfReadTestBytes 141 | if file.f.Size() < numberOfReadTestBytes { 142 | bytesToRead = file.f.Size() 143 | } 144 | 145 | fi, err := os.Open(mode.absolutePath + "/" + file.f.Name()) 146 | if err != nil { 147 | return false, err 148 | } 149 | defer fi.Close() 150 | 151 | buf := make([]byte, bytesToRead) 152 | _, err = fi.Read(buf) 153 | if err != nil { 154 | return false, err 155 | } 156 | for _, b := range buf { 157 | if b == byte(0) { 158 | return false, nil 159 | } 160 | } 161 | return true, nil 162 | } 163 | 164 | func investigateGit(path string) string { 165 | const UrlLine = "url = " 166 | buf, err := ioutil.ReadFile(path + "/.git/config") 167 | if err != nil { 168 | return "" 169 | } 170 | cfg := string(buf) 171 | i := strings.Index(cfg, UrlLine) 172 | if i == -1 { 173 | return "" 174 | } 175 | j := strings.Index(cfg[i:], "\n") 176 | if j == -1 { 177 | return "" 178 | } 179 | return "[FILENAME]" + cfg[i+len(UrlLine):i+j] + "[DEFAULT]" 180 | } 181 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // lsp.go contains main() function 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | ) 9 | 10 | func main() { 11 | mode, err := ParseArguments(os.Args) 12 | if err != nil { 13 | fmt.Printf("Unable to find directory \"%s\" : error %s \n", presentPath(mode.inputPath), err) 14 | return 15 | } 16 | files, err := ioutil.ReadDir(mode.absolutePath) 17 | if err != nil { 18 | fmt.Printf("Unable to list directory: %s \n", err) 19 | return 20 | } 21 | if len(files) == 0 { 22 | fmt.Printf(ColorScheme.Color(fmt.Sprintf("[META]lsp[DEFAULT]: dir [FILENAME]%s[DEFAULT] is empty\n", mode.absolutePath))) 23 | return 24 | } 25 | 26 | FileList = researchFileList(files) 27 | populateTrie() 28 | render() 29 | } 30 | -------------------------------------------------------------------------------- /paths.go: -------------------------------------------------------------------------------- 1 | // paths.go contains filesystem path parsing and manipulation operations 2 | package main 3 | 4 | import ( 5 | "os/user" 6 | "strings" 7 | ) 8 | 9 | func presentPath(path string) string { 10 | usr, _ := user.Current() 11 | homeDir := usr.HomeDir 12 | if !strings.HasPrefix(path, homeDir) { 13 | return path 14 | } 15 | return strings.Replace(path, homeDir, "~", 1) 16 | } 17 | -------------------------------------------------------------------------------- /sort.go: -------------------------------------------------------------------------------- 1 | // sort.go contains mode-specific file sorting interface implementations 2 | package main 3 | 4 | import ( 5 | "strings" 6 | "unicode" 7 | "unicode/utf8" 8 | ) 9 | 10 | type alphabeticSort []*FileInfo 11 | 12 | func (a alphabeticSort) Len() int { return len(a) } 13 | func (a alphabeticSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 14 | func (a alphabeticSort) Less(i, j int) bool { 15 | r1 := strings.NewReader(a[j].f.Name()) 16 | r2 := strings.NewReader(a[i].f.Name()) 17 | for { 18 | ch1 := nextRune(r1) 19 | ch2 := nextRune(r2) 20 | if ch1 == ch2 { 21 | if ch1 == utf8.RuneError { 22 | return true 23 | } 24 | continue 25 | } 26 | if ch1 > ch2 { 27 | return true 28 | } 29 | return false 30 | } 31 | } 32 | 33 | type sizeSort []*FileInfo 34 | 35 | func (a sizeSort) Len() int { return len(a) } 36 | func (a sizeSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 37 | func (a sizeSort) Less(i, j int) bool { 38 | return a[i].f.Size() > a[j].f.Size() 39 | } 40 | 41 | type timeSort []*FileInfo 42 | 43 | func (a timeSort) Len() int { return len(a) } 44 | func (a timeSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 45 | func (a timeSort) Less(i, j int) bool { 46 | return a[i].f.ModTime().After(a[j].f.ModTime()) 47 | } 48 | 49 | func nextRune(r *strings.Reader) rune { 50 | ch, _, err := r.ReadRune() 51 | if err != nil { 52 | return utf8.RuneError 53 | } 54 | return unicode.ToLower(ch) 55 | } 56 | -------------------------------------------------------------------------------- /traverse.go: -------------------------------------------------------------------------------- 1 | // traverse group Trie and select representation grouping 2 | package main 3 | 4 | import "fmt" 5 | 6 | // Traverse traverses file group Trie 7 | func Traverse() { 8 | ch := make(chan traversePos) 9 | go func() { 10 | Trie.Walk(ch, []string{}) 11 | close(ch) 12 | }() 13 | 14 | printHR() 15 | for leave := range ch { 16 | if !mode.summary { 17 | fmt.Printf("\n") 18 | printHeader(nameTriePath(leave.Keys)) 19 | } 20 | renderFiles(leave.Fls) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /trie.go: -------------------------------------------------------------------------------- 1 | // trie.go contains a Trie of grouped/classified files with attributes as nodes. 2 | // Root node has 3 parents: directories, reg. files and special files (everything else, like symlinks, devices and so on) 3 | // it goes down up to file extension, modification time and so on 4 | // 5 | // this allows to show files reasonably grouped: 6 | // if there is a bunch of text files with different extension 7 | // they will be rendered grouped as "reg text files" (instead of having a separate group for each file) 8 | // but 9 | // if there is a bunch of files for each of two files of extension 10 | // the two groups and the common attribute for each will be shown 11 | // 12 | // all we need to do is to select node that has reasonable number of files among the children leaves (at least five) 13 | package main 14 | 15 | // Node is a trie node 16 | type Node struct { 17 | Ch map[string]*Node // children nodes mapped with string label 18 | Fls []*FileInfo //files on this node 19 | } 20 | 21 | // GetNode returns children node with 'key' key. if such a node does not exist yet, it is created first 22 | func (n *Node) GetNode(key string) *Node { 23 | if n.Ch == nil { 24 | n.Ch = make(map[string]*Node) 25 | } 26 | node, ok := n.Ch[key] 27 | if ok { 28 | return node 29 | } 30 | n.Ch[key] = &Node{} 31 | return n.Ch[key] 32 | } 33 | 34 | // AddFile adds file reference to Node's file list (while initializing file if needed) 35 | func (n *Node) AddFile(fl *FileInfo) { 36 | 37 | } 38 | 39 | type traversePos struct { 40 | Fls []*FileInfo 41 | Keys []string 42 | } 43 | 44 | // Walk traverses node DFS-style and sends non-empty n.Fls to channel ch 45 | func (n *Node) Walk(ch chan traversePos, keys []string) { 46 | if n.Fls != nil && len(n.Fls) > 0 { 47 | ch <- traversePos{n.Fls, keys} 48 | } 49 | if n.Ch == nil { 50 | return 51 | } 52 | 53 | for k, childNode := range n.Ch { 54 | childNode.Walk(ch, append(keys, k)) 55 | } 56 | } 57 | 58 | // Trie contains classified files 59 | var Trie = Node{ 60 | Ch: map[string]*Node{ 61 | "dirs": &Node{}, 62 | "special": &Node{}, 63 | "regulars": &Node{Ch: map[string]*Node{ 64 | "executables": &Node{}, 65 | "blobs": &Node{}, 66 | "text": &Node{}, 67 | "empty": &Node{}, 68 | }}, 69 | }} 70 | 71 | func populateTrie() { 72 | var n *Node 73 | for i, f := range FileList { 74 | switch f.special { 75 | case "": 76 | n = Trie.GetNode("regulars") 77 | case "Text File": 78 | n = Trie.GetNode("regulars").GetNode("text") 79 | case "Binary File": 80 | n = Trie.GetNode("regulars").GetNode("blobs") 81 | case "Empty File": 82 | n = Trie.GetNode("regulars").GetNode("empty") 83 | case "dir": 84 | n = Trie.GetNode("dirs") 85 | default: 86 | n = Trie.Ch["special"].GetNode(f.special) 87 | } 88 | n.Fls = append(n.Fls, &FileList[i]) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tty_unix.go: -------------------------------------------------------------------------------- 1 | // +build darwin freebsd openbsd linux 2 | 3 | package main 4 | 5 | import ( 6 | "syscall" 7 | "unsafe" 8 | ) 9 | 10 | // SetColumnSize attempts to read the dimensions of the given terminal. 11 | func SetColumnSize() { 12 | const stdoutFD = 1 13 | var dimensions [4]uint16 14 | 15 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(stdoutFD), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0); err != 0 { 16 | return 17 | } 18 | 19 | terminalWidth = int(dimensions[1]) 20 | if terminalWidth < 3 { 21 | return 22 | } 23 | columnSize = (terminalWidth - 2) / 2 24 | } 25 | -------------------------------------------------------------------------------- /tty_win.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package main 4 | 5 | // SetColumnSize attempts to read the dimensions of the given terminal. 6 | func SetColumnSize() { 7 | const stdoutFD = 1 8 | 9 | terminalWidth = 40 10 | columnSize = (terminalWidth - 2) / 2 11 | } 12 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | // Package utils.go contains various helpful functions and definitions 2 | package main 3 | 4 | import "time" 5 | 6 | var timeout = make(chan bool, 1) 7 | 8 | func setTimeoutTimer() { 9 | go func() { 10 | time.Sleep(500 * time.Millisecond) 11 | timeout <- true 12 | }() 13 | } 14 | --------------------------------------------------------------------------------