├── man ├── man_test.go ├── g.1.gz └── man.go ├── internal ├── global │ ├── var.go │ ├── doc │ │ ├── doc.go │ │ └── nodoc.go │ ├── debug │ │ ├── disable.go │ │ └── enable.go │ └── const.go ├── cli │ ├── version.go │ └── index.go ├── osbased │ ├── newline_unix.go │ ├── newline_windows.go │ ├── flags_linux.go │ ├── flags_windows.go │ ├── time_linux_64.go │ ├── time_linux_32.go │ ├── time_windows.go │ ├── time_darwin.go │ ├── usergroup_darwin.go │ ├── usergroup_linux.go │ ├── filedetail_linux.go │ ├── flags_darwin.go │ ├── filedetail_darwin.go │ ├── filedetail_windows.go │ ├── macos_alias.h │ └── usergroup_windows.go ├── theme │ ├── sys_darwin.go │ ├── sys_linux.go │ ├── colorless.go │ ├── custom_builtin.go │ ├── sys_windows.go │ ├── defaultinit_test.go │ ├── defaultini.go │ ├── colorless_test.go │ ├── default_test.go │ └── color_test.go ├── item │ ├── item.go │ └── fileinfo.go ├── util │ ├── set_test.go │ ├── set.go │ ├── once_test.go │ ├── once.go │ ├── safeSlice.go │ ├── safeSlice_test.go │ ├── util.go │ ├── file_test.go │ ├── file.go │ ├── util_test.go │ ├── jaro.go │ └── termlink.go ├── content │ ├── mounts_lite.go │ ├── index.go │ ├── flags.go │ ├── link.go │ ├── inode.go │ ├── mounts.go │ ├── permission.go │ ├── owner.go │ ├── size_test.go │ ├── group.go │ ├── sum.go │ ├── charset.go │ ├── mimetype.go │ ├── time.go │ ├── contentfilter.go │ ├── name_test.go │ ├── git.go │ └── duplicate.go ├── git │ ├── git.go │ ├── ignoredcache.go │ ├── git_commit.go │ ├── git_status_test.go │ └── git_status.go ├── align │ ├── left_test.go │ └── left.go ├── shell │ ├── g.fish │ ├── g.zsh │ ├── integration_test.go │ ├── g.bash │ ├── g.nu │ ├── g.ps1 │ └── integration.go ├── display │ ├── item.go │ ├── tree │ │ └── tree.go │ └── header.go ├── index │ ├── pathindex_lite.go │ └── pathindex.go ├── cached │ ├── cached.go │ ├── cached_test.go │ └── cachedmaps.go └── config │ ├── load_test.go │ └── load.go ├── asset └── screenshot_3.png ├── script ├── base.sh ├── theme_test.sh ├── new_test.sh ├── install_dev_requirement.sh ├── run_test.sh └── reproduce_test_result.sh ├── .gitignore ├── README_EN.md ├── .github ├── ranger.yml ├── workflows │ ├── lint.yml │ ├── gofumpt.yml │ └── go.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── style-request.md │ └── feature-request.md ├── LICENSE ├── docs ├── Theme.md ├── TestWorkflow.md ├── BuildOption.md └── ReleaseWorkflow.md ├── scoop ├── g.json └── scoop.sh ├── completions ├── bash │ └── g-completion.bash ├── fish │ └── g.fish └── zsh │ └── _g ├── go.mod ├── CONTRIBUTING.md ├── main.go └── CODE_OF_CONDUCT.md /man/man_test.go: -------------------------------------------------------------------------------- 1 | package man 2 | -------------------------------------------------------------------------------- /internal/global/var.go: -------------------------------------------------------------------------------- 1 | package global 2 | -------------------------------------------------------------------------------- /internal/cli/version.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | var Version = "0.31.0" 4 | -------------------------------------------------------------------------------- /man/g.1.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Equationzhao/g/HEAD/man/g.1.gz -------------------------------------------------------------------------------- /asset/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Equationzhao/g/HEAD/asset/screenshot_3.png -------------------------------------------------------------------------------- /internal/global/doc/doc.go: -------------------------------------------------------------------------------- 1 | //go:build doc 2 | 3 | package doc 4 | 5 | const Enable = true 6 | -------------------------------------------------------------------------------- /internal/global/debug/disable.go: -------------------------------------------------------------------------------- 1 | //go:build !debug 2 | 3 | package debug 4 | 5 | const Enable = false 6 | -------------------------------------------------------------------------------- /internal/global/debug/enable.go: -------------------------------------------------------------------------------- 1 | //go:build debug 2 | 3 | package debug 4 | 5 | const Enable = true 6 | -------------------------------------------------------------------------------- /internal/osbased/newline_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package osbased 4 | 5 | const ( 6 | Newline = "\n" 7 | OtherNewline = "\r\n" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/osbased/newline_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package osbased 4 | 5 | const ( 6 | Newline = "\r\n" 7 | OtherNewline = "\n" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/osbased/flags_linux.go: -------------------------------------------------------------------------------- 1 | package osbased 2 | 3 | import "github.com/Equationzhao/g/internal/item" 4 | 5 | func CheckFlags(_ *item.FileInfo) []string { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /internal/osbased/flags_windows.go: -------------------------------------------------------------------------------- 1 | package osbased 2 | 3 | import "github.com/Equationzhao/g/internal/item" 4 | 5 | func CheckFlags(_ *item.FileInfo) []string { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /internal/theme/sys_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package theme 4 | 5 | func init() { 6 | DefaultAll.Name["system"] = Style{ 7 | Icon: "\uF179", 8 | Color: dir, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /script/base.sh: -------------------------------------------------------------------------------- 1 | error() { 2 | printf '\033[1;31m%s\033[0m\n' "$1" 3 | } 4 | 5 | success() { 6 | printf '\033[1;32m%s\033[0m\n' "$1" 7 | } 8 | 9 | warn() { 10 | printf '\033[1;33m%s\033[0m\n' "$1" 11 | } -------------------------------------------------------------------------------- /internal/item/item.go: -------------------------------------------------------------------------------- 1 | package item 2 | 3 | type Item interface { 4 | String() string 5 | NO() int // NO return the No. of item 6 | SetPrefix(string) 7 | SetSuffix(string) 8 | AddPrefix(string) 9 | AddSuffix(string) 10 | } 11 | -------------------------------------------------------------------------------- /internal/util/set_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSafeSet(t *testing.T) { 8 | set := NewSet[string]() 9 | set.Add("name") 10 | if !set.Contains("name") { 11 | t.Errorf("Add failed") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /internal/theme/sys_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package theme 4 | 5 | import "github.com/Equationzhao/g/internal/global" 6 | 7 | func init() { 8 | DefaultAll.Name["sys"] = Style{ 9 | Icon: "\ue712", 10 | Color: global.BrightBlue, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ide 2 | .vscode/ 3 | .history/ 4 | .idea/ 5 | .fleet/ 6 | 7 | # system 8 | .DS_Store 9 | 10 | # binary 11 | g 12 | g.exe 13 | g-git 14 | g-git.exe 15 | build/ 16 | 17 | # temp file 18 | *.swp 19 | 20 | # compress 21 | *.gz 22 | 23 | # python 24 | __pycache__/ 25 | *.pyc 26 | 27 | example/ 28 | -------------------------------------------------------------------------------- /internal/content/mounts_lite.go: -------------------------------------------------------------------------------- 1 | //go:build !mounts 2 | 3 | package content 4 | 5 | // Lite version without mounts functionality 6 | // This reduces binary size by removing gopsutil dependency 7 | 8 | func MountsOn(path string) string { 9 | // In lite build, return empty string (no mount info) 10 | return "" 11 | } 12 | -------------------------------------------------------------------------------- /internal/global/doc/nodoc.go: -------------------------------------------------------------------------------- 1 | //go:build !doc 2 | 3 | // Package doc is used to generate docs 4 | // when `-tags doc` is passed to go build 5 | // Enable = true, and the corresponding logic in the main.go will be executed 6 | // man.md and man will be generated 7 | // by default, Enable = false 8 | package doc 9 | 10 | const Enable = false 11 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # g 2 | 3 | --- 4 | 5 | A feature-rich, customizable, and cross-platform `ls` alternative. 6 | 7 | Experience enhanced visuals with type-specific icons, various layout options, and git status integration. 8 | 9 | --- 10 | 11 |

12 | this document has been moved to README.md 13 |

-------------------------------------------------------------------------------- /.github/ranger.yml: -------------------------------------------------------------------------------- 1 | default: 2 | close: 3 | comment: '⚠️ This has been marked $LABEL and will be closed in $DELAY.' 4 | delay: '15 days' 5 | 6 | labels: 7 | duplicate: close 8 | wontfix: close 9 | invalid: close 10 | 'merge when passing': merge 11 | 'squash when passing': merge 12 | 'rebase when passing': merge 13 | 14 | merges: 15 | - action: delete_branch -------------------------------------------------------------------------------- /script/theme_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # load base.sh 4 | source "$(dirname "$0")/base.sh" 5 | 6 | temp_build_name="custom_theme_test_g_"$RANDOM 7 | echo "go build -tags=custom -o $temp_build_name" 8 | go build -tags=custom -o $temp_build_name . 9 | if [ $? -ne 0 ]; then 10 | error "build failed" 11 | exit 1 12 | fi 13 | success "build success" 14 | rm $temp_build_name -------------------------------------------------------------------------------- /internal/theme/colorless.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | func SetClassic() { 4 | DefaultAll.Apply(setClassic) 5 | } 6 | 7 | func setClassic(m Theme) { 8 | for k := range m { 9 | m[k] = Style{ 10 | Icon: m[k].Icon, 11 | Color: "", 12 | Underline: false, 13 | Bold: false, 14 | Faint: false, 15 | Italics: false, 16 | Blink: false, 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /man/man.go: -------------------------------------------------------------------------------- 1 | package man 2 | 3 | import ( 4 | "compress/gzip" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/Equationzhao/g/internal/cli" 9 | ) 10 | 11 | func GenMan() { 12 | // man 13 | man, _ := os.Create(filepath.Join("man", "g.1.gz")) 14 | s, _ := cli.G.ToMan() 15 | // compress to gzip 16 | manGz := gzip.NewWriter(man) 17 | defer manGz.Close() 18 | _, _ = manGz.Write([]byte(s)) 19 | _ = manGz.Flush() 20 | } 21 | -------------------------------------------------------------------------------- /internal/theme/custom_builtin.go: -------------------------------------------------------------------------------- 1 | //go:build custom 2 | 3 | package theme 4 | 5 | import ( 6 | _ "embed" 7 | ) 8 | 9 | //go:embed custom_builtin.json 10 | var customThemeJson []byte 11 | 12 | func init() { 13 | // read the first line of customThemeIni 14 | a, err, fatal := getTheme(customThemeJson) 15 | if fatal != nil { 16 | panic(fatal) 17 | } 18 | if err != nil { 19 | panic(err) 20 | } 21 | DefaultAll = a 22 | _init = true 23 | } 24 | -------------------------------------------------------------------------------- /internal/content/index.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | constval "github.com/Equationzhao/g/internal/global" 5 | "github.com/Equationzhao/g/internal/item" 6 | ) 7 | 8 | type IndexEnabler struct{} 9 | 10 | func NewIndexEnabler() *IndexEnabler { 11 | return &IndexEnabler{} 12 | } 13 | 14 | func (i *IndexEnabler) Enable() ContentOption { 15 | return func(info *item.FileInfo) (string, string) { 16 | return "", constval.NameOfIndex 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/git/git.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "os/exec" 5 | "strings" 6 | ) 7 | 8 | func getTopLevel(path RepoPath) (string, error) { 9 | c := exec.Command("git", "rev-parse", "--show-toplevel", path) 10 | c.Dir = path 11 | out, err := c.Output() 12 | if err == nil { 13 | // get the first line 14 | lines := strings.Split(string(out), "\n") 15 | if len(lines) > 0 { 16 | return lines[0], nil 17 | } 18 | } 19 | return "", err 20 | } 21 | -------------------------------------------------------------------------------- /internal/align/left_test.go: -------------------------------------------------------------------------------- 1 | package align 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zeebo/assert" 7 | ) 8 | 9 | func TestIsLeft(t *testing.T) { 10 | Register("name") 11 | assert.Equal(t, true, IsLeft("name")) 12 | assert.Equal(t, false, IsLeft("name1")) 13 | } 14 | 15 | func TestIsLeftHeaderFooter(t *testing.T) { 16 | RegisterHeaderFooter("name") 17 | assert.Equal(t, true, IsLeftHeaderFooter("name")) 18 | assert.Equal(t, false, IsLeftHeaderFooter("name1")) 19 | } 20 | -------------------------------------------------------------------------------- /internal/shell/g.fish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/fish 2 | 3 | if command -v g >/dev/null 2>&1 4 | functions -e ll 5 | functions -e l 6 | functions -e la 7 | functions -e ls 8 | alias ls 'g' 9 | alias ll 'g --perm --icons --time --group --owner --size --title' 10 | alias l 'g --perm --icons --time --group --owner --size --title --show-hidden' 11 | alias la 'g --show-hidden' 12 | end 13 | 14 | # add to fish config: 15 | # g --init fish | source 16 | # 'source ~/.config/fish/config.fish' 17 | -------------------------------------------------------------------------------- /internal/theme/sys_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package theme 4 | 5 | func init() { 6 | DefaultAll.Special["system"] = Style{ 7 | Icon: "\uE70F", 8 | Color: dir, 9 | } 10 | DefaultAll.Group["devtoolsuser"] = Style{ 11 | Color: color256(202), 12 | } 13 | DefaultAll.Name["program files"] = Style{ 14 | Icon: "\ueb44", 15 | } 16 | DefaultAll.Name["program files (x86)"] = Style{ 17 | Icon: "\ueb44", 18 | } 19 | DefaultAll.Name["windows"] = Style{ 20 | Icon: "\uE70F", 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/shell/g.zsh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # alias for g 4 | if [ "$(command -v g)" ]; then 5 | unalias -m 'll' 6 | unalias -m 'l' 7 | unalias -m 'la' 8 | unalias -m 'ls' 9 | alias ls='g' 10 | alias ll='g --perm --icons --time --group --owner --size --title' 11 | alias l='g --perm --icons --time --group --owner --size --title --show-hidden' 12 | alias la='g --show-hidden' 13 | fi 14 | 15 | # add the following command to .zshrc 16 | # eval "$(g --init zsh)" 17 | # then 'source ~/.zshrc' 18 | -------------------------------------------------------------------------------- /internal/shell/integration_test.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import "testing" 4 | 5 | func TestInit(t *testing.T) { 6 | shells := []string{"bash", "zsh", "fish", "powershell", "pwsh", "nushell", "nu"} 7 | for _, shell := range shells { 8 | _, err := Init(shell) 9 | if err != nil { 10 | t.Errorf("unexpected error: %v", err) 11 | } 12 | } 13 | init, err := Init("unknown") 14 | if init != "" { 15 | t.Errorf("expected empty string, got %s", init) 16 | } 17 | if err == nil { 18 | t.Errorf("expected error, got nil") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/util/set.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | constval "github.com/Equationzhao/g/internal/global" 5 | "github.com/alphadose/haxmap" 6 | ) 7 | 8 | type SafeSet[T constval.Hashable] struct { 9 | internal *haxmap.Map[T, struct{}] 10 | } 11 | 12 | func (s *SafeSet[T]) Add(k T) { 13 | s.internal.Set(k, struct{}{}) 14 | } 15 | 16 | func (s *SafeSet[T]) Contains(k T) bool { 17 | _, t := s.internal.Get(k) 18 | return t 19 | } 20 | 21 | func NewSet[T constval.Hashable]() *SafeSet[T] { 22 | return &SafeSet[T]{ 23 | internal: haxmap.New[T, struct{}](10), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint check 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | golangci: 16 | name: lint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-go@v5 21 | with: 22 | go-version: 1.24.0 23 | - uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 # v6.5.0 24 | with: 25 | install-mode: "binary" -------------------------------------------------------------------------------- /internal/util/once_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | ) 7 | 8 | func TestOnce_Do(t *testing.T) { 9 | var o Once 10 | var count int 11 | 12 | wg := sync.WaitGroup{} 13 | times := 10 14 | wg.Add(times) 15 | for i := 0; i < times; i++ { 16 | go func() { 17 | defer wg.Done() 18 | err := o.Do(func() error { 19 | count++ 20 | return nil 21 | }) 22 | if err != nil { 23 | t.Errorf("expected nil, got %v", err) 24 | } 25 | }() 26 | } 27 | wg.Wait() 28 | if count != 1 { 29 | t.Errorf("expected 1, got %d", count) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internal/osbased/time_linux_64.go: -------------------------------------------------------------------------------- 1 | //go:build (amd64 || arm64 || loong64) && linux 2 | 3 | package osbased 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | "time" 9 | ) 10 | 11 | func ModTime(a os.FileInfo) time.Time { 12 | return a.ModTime() 13 | } 14 | 15 | func AccessTime(a os.FileInfo) time.Time { 16 | atim := a.Sys().(*syscall.Stat_t).Atim 17 | return time.Unix(atim.Sec, atim.Nsec) 18 | } 19 | 20 | func CreateTime(a os.FileInfo) time.Time { 21 | ctim := a.Sys().(*syscall.Stat_t).Ctim 22 | return time.Unix(ctim.Sec, ctim.Nsec) 23 | } 24 | 25 | func BirthTime(a os.FileInfo) time.Time { 26 | return CreateTime(a) 27 | } 28 | -------------------------------------------------------------------------------- /internal/shell/g.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$(command -v g)" ]; then 4 | if [ "$(command -v ll)" ]; then 5 | unalias ll 6 | fi 7 | 8 | if [ "$(command -v l)" ]; then 9 | unalias l 10 | fi 11 | 12 | if [ "$(command -v la)" ]; then 13 | unalias la 14 | fi 15 | 16 | alias ls='g' 17 | alias ll='g --perm --icons --time --group --owner --size --title' 18 | alias l='g --perm --icons --time --group --owner --size --title --show-hidden' 19 | alias la='g --show-hidden' 20 | fi 21 | 22 | # add the following command to .bashrc 23 | # eval "$(g --init bash)" 24 | # then 'source ~/.bashrc' 25 | -------------------------------------------------------------------------------- /internal/osbased/time_linux_32.go: -------------------------------------------------------------------------------- 1 | //go:build (arm || 386) && linux 2 | 3 | package osbased 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | "time" 9 | ) 10 | 11 | func ModTime(a os.FileInfo) time.Time { 12 | return a.ModTime() 13 | } 14 | 15 | func AccessTime(a os.FileInfo) time.Time { 16 | atim := a.Sys().(*syscall.Stat_t).Atim 17 | return time.Unix(int64(atim.Sec), int64(atim.Nsec)) 18 | } 19 | 20 | func CreateTime(a os.FileInfo) time.Time { 21 | ctim := a.Sys().(*syscall.Stat_t).Ctim 22 | return time.Unix(int64(ctim.Sec), int64(ctim.Nsec)) 23 | } 24 | 25 | func BirthTime(a os.FileInfo) time.Time { 26 | return CreateTime(a) 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | 1. What entries you're listing 15 | 2. What flags you set 16 | 17 | **Screenshots** 18 | If applicable, add screenshots to help explain your problem. 19 | 20 | **Info (please complete the following information):** 21 | - OS: [e.g. macOS 14.0] 22 | - Version [e.g. v0.18.1] 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /internal/osbased/time_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package osbased 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | "time" 9 | ) 10 | 11 | func ModTime(a os.FileInfo) time.Time { 12 | return a.ModTime() 13 | } 14 | 15 | func AccessTime(a os.FileInfo) time.Time { 16 | ctim := a.Sys().(*syscall.Win32FileAttributeData).LastAccessTime 17 | return time.Unix(0, ctim.Nanoseconds()) 18 | } 19 | 20 | func CreateTime(a os.FileInfo) time.Time { 21 | atim := a.Sys().(*syscall.Win32FileAttributeData).CreationTime 22 | return time.Unix(0, atim.Nanoseconds()) 23 | } 24 | 25 | func BirthTime(a os.FileInfo) time.Time { 26 | return CreateTime(a) 27 | } 28 | -------------------------------------------------------------------------------- /internal/shell/g.nu: -------------------------------------------------------------------------------- 1 | alias ls = ^g 2 | alias ll = ^g --perm --icons --time --group --owner --size --title 3 | alias l = ^g --perm --icons --time --group --owner --size --title --show-hidden 4 | alias la = ^g --show-hidden 5 | 6 | # add the following to your $nu.env-path 7 | # ^g --init nushell | save -f ~/.g.nu 8 | # then add the following to your $nu.config-path 9 | # source ~/.g.nu 10 | # if you want to replace nushell's g command with g 11 | # add the following definition and alias to your $nu.config-path 12 | # 13 | # def nug [arg?] { 14 | # if ($arg == null) { 15 | # g $arg 16 | # } else { 17 | # g 18 | # } 19 | # } 20 | # alias g = ^g 21 | -------------------------------------------------------------------------------- /internal/osbased/time_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package osbased 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | "time" 9 | ) 10 | 11 | func ModTime(a os.FileInfo) time.Time { 12 | return a.ModTime() 13 | } 14 | 15 | func AccessTime(a os.FileInfo) time.Time { 16 | atim := a.Sys().(*syscall.Stat_t).Atimespec 17 | return time.Unix(atim.Sec, atim.Nsec) 18 | } 19 | 20 | func CreateTime(a os.FileInfo) time.Time { 21 | ctim := a.Sys().(*syscall.Stat_t).Ctimespec 22 | return time.Unix(ctim.Sec, ctim.Nsec) 23 | } 24 | 25 | func BirthTime(a os.FileInfo) time.Time { 26 | btim := a.Sys().(*syscall.Stat_t).Birthtimespec 27 | return time.Unix(btim.Sec, btim.Nsec) 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/style-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Style request 3 | about: Suggest a new Style 4 | title: "[STYLE]" 5 | labels: style 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the Style you'd like to add** 11 | Color: [eg. Yellow] 12 | Icon: [eg. "\uF48A"] 13 | Underline: [eg. true] 14 | Bold: [eg. true] 15 | 16 | Target: eg: `EXT: md` or `name: readme` 17 | 18 | **Describe the file type** 19 | eg: a [README](https://en.wikipedia.org/wiki/README) file contains information about the other files in a directory or archive of computer software. / [Markdown](https://en.wikipedia.org/wiki/Markdown) is a lightweight markup language for creating formatted text using a plain-text editor. 20 | -------------------------------------------------------------------------------- /internal/shell/g.ps1: -------------------------------------------------------------------------------- 1 | Remove-Alias -Name ls 2 | function ls { 3 | param( 4 | [Switch] $path 5 | ) 6 | g $args 7 | } 8 | 9 | function ll { 10 | g -1 --perm --icons --time --group --owner --size --title 11 | } 12 | 13 | function la { 14 | g --show-hidden 15 | } 16 | 17 | function l { 18 | g --perm --icons --time --group --owner --size --title --show-hidden 19 | } 20 | 21 | # `echo $profile` in PowerShell to find your profile path 22 | # add the following line to your profile 23 | # Invoke-Expression (& { (g --init powershell | Out-String) }) 24 | # if you've already remove alias `ls`, you can comment out the first line 25 | # and paste the content to your profile manually 26 | -------------------------------------------------------------------------------- /internal/util/once.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | type Once struct { 9 | m sync.RWMutex 10 | done uint32 11 | } 12 | 13 | func (o *Once) Do(fn func() error) error { 14 | if atomic.LoadUint32(&o.done) == 1 { 15 | return nil 16 | } 17 | return o.doSlow(fn) 18 | } 19 | 20 | func (o *Once) doSlow(fn func() error) error { 21 | o.m.RLock() 22 | var err error 23 | if o.done == 0 { 24 | o.m.RUnlock() 25 | o.m.Lock() 26 | 27 | defer o.m.Unlock() 28 | if o.done == 0 { 29 | err = fn() 30 | if err == nil { 31 | atomic.StoreUint32(&o.done, 1) 32 | } 33 | } 34 | 35 | } else { 36 | o.m.RUnlock() 37 | } 38 | return err 39 | } 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEAT]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /internal/align/left.go: -------------------------------------------------------------------------------- 1 | package align 2 | 3 | import ( 4 | "github.com/Equationzhao/g/internal/util" 5 | ) 6 | 7 | // left 8 | // the default is right align 9 | // field to align left should register here 10 | var left = util.NewSet[string]() 11 | 12 | func Register(names ...string) { 13 | for _, name := range names { 14 | left.Add(name) 15 | } 16 | } 17 | 18 | func IsLeft(name string) bool { 19 | return left.Contains(name) 20 | } 21 | 22 | var leftHeaderFooter = util.NewSet[string]() 23 | 24 | func RegisterHeaderFooter(names ...string) { 25 | for _, name := range names { 26 | leftHeaderFooter.Add(name) 27 | } 28 | } 29 | 30 | func IsLeftHeaderFooter(name string) bool { 31 | return leftHeaderFooter.Contains(name) 32 | } 33 | -------------------------------------------------------------------------------- /internal/osbased/usergroup_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package osbased 4 | 5 | import ( 6 | "os" 7 | "strconv" 8 | "syscall" 9 | 10 | "github.com/Equationzhao/g/internal/cached" 11 | ) 12 | 13 | func GroupID(a os.FileInfo) string { 14 | return strconv.FormatInt(int64(a.Sys().(*syscall.Stat_t).Gid), 10) 15 | } 16 | 17 | func Group(a os.FileInfo) string { 18 | return cached.GetGroupname(strconv.FormatInt(int64(a.Sys().(*syscall.Stat_t).Gid), 10)) 19 | } 20 | 21 | func OwnerID(a os.FileInfo) string { 22 | return strconv.FormatInt(int64(a.Sys().(*syscall.Stat_t).Uid), 10) 23 | } 24 | 25 | func Owner(a os.FileInfo) string { 26 | return cached.GetUsername(strconv.FormatInt(int64(a.Sys().(*syscall.Stat_t).Uid), 10)) 27 | } 28 | -------------------------------------------------------------------------------- /internal/osbased/usergroup_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package osbased 4 | 5 | import ( 6 | "os" 7 | "strconv" 8 | "syscall" 9 | 10 | "github.com/Equationzhao/g/internal/cached" 11 | ) 12 | 13 | func GroupID(a os.FileInfo) string { 14 | return strconv.FormatInt(int64(a.Sys().(*syscall.Stat_t).Gid), 10) 15 | } 16 | 17 | func Group(a os.FileInfo) string { 18 | return cached.GetGroupname(strconv.FormatInt(int64(a.Sys().(*syscall.Stat_t).Gid), 10)) 19 | } 20 | 21 | func OwnerID(a os.FileInfo) string { 22 | return strconv.FormatInt(int64(a.Sys().(*syscall.Stat_t).Uid), 10) 23 | } 24 | 25 | func Owner(a os.FileInfo) string { 26 | return cached.GetUsername(strconv.FormatInt(int64(a.Sys().(*syscall.Stat_t).Uid), 10)) 27 | } 28 | -------------------------------------------------------------------------------- /internal/content/flags.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/Equationzhao/g/internal/align" 7 | constval "github.com/Equationzhao/g/internal/global" 8 | "github.com/Equationzhao/g/internal/item" 9 | "github.com/Equationzhao/g/internal/osbased" 10 | ) 11 | 12 | type FlagsEnabler struct{} 13 | 14 | func NewFlagsEnabler() *FlagsEnabler { 15 | return &FlagsEnabler{} 16 | } 17 | 18 | const ( 19 | Flags = constval.NameOfFlags 20 | ) 21 | 22 | func (f FlagsEnabler) Enable() ContentOption { 23 | align.Register(Flags) 24 | return func(info *item.FileInfo) (string, string) { 25 | flags := osbased.CheckFlags(info) 26 | if len(flags) == 0 { 27 | return "-", Flags 28 | } 29 | return strings.Join(flags, ","), Flags 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internal/theme/defaultinit_test.go: -------------------------------------------------------------------------------- 1 | //go:build theme 2 | 3 | package theme 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/Equationzhao/g/internal/global" 9 | ) 10 | 11 | func TestStyle_ToReadable(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | before Style 15 | want Style 16 | }{ 17 | { 18 | name: "TestStyle_ToReadable", 19 | before: Style{ 20 | Color: global.BrightBlue, 21 | }, 22 | want: Style{ 23 | Color: "bright-blue", 24 | }, 25 | }, 26 | } 27 | 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | got := tt.before.ToReadable() 31 | if got.Color != tt.want.Color { 32 | t.Errorf("Style.ToReadable() = %v, want %v", got.Color, tt.want.Color) 33 | } 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/gofumpt.yml: -------------------------------------------------------------------------------- 1 | name: gofumpt format check 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-go@v5 21 | with: 22 | go-version: 1.24.0 23 | 24 | - name: Install gofumpt 25 | run: go install mvdan.cc/gofumpt@latest 26 | 27 | - name: Run gofumpt and check formatting 28 | run: | 29 | output=$(gofumpt -l -extra .) 30 | if [ -n "$output" ]; then 31 | echo "$output" 32 | exit 1 33 | else 34 | echo "pass" 35 | fi 36 | -------------------------------------------------------------------------------- /internal/content/link.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/Equationzhao/g/internal/align" 7 | 8 | constval "github.com/Equationzhao/g/internal/global" 9 | "github.com/Equationzhao/g/internal/item" 10 | "github.com/Equationzhao/g/internal/osbased" 11 | "github.com/Equationzhao/g/internal/render" 12 | ) 13 | 14 | // LinkEnabler List each file's number of hard links. 15 | type LinkEnabler struct{} 16 | 17 | func NewLinkEnabler() *LinkEnabler { 18 | return &LinkEnabler{} 19 | } 20 | 21 | const Link = constval.NameOfLink 22 | 23 | func (l *LinkEnabler) Enable(renderer *render.Renderer) ContentOption { 24 | align.RegisterHeaderFooter(Link) 25 | return func(info *item.FileInfo) (string, string) { 26 | return renderer.Link(strconv.FormatUint(osbased.LinkCount(info), 10)), Link 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/content/inode.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "github.com/Equationzhao/g/internal/align" 5 | constval "github.com/Equationzhao/g/internal/global" 6 | "github.com/Equationzhao/g/internal/item" 7 | "github.com/Equationzhao/g/internal/osbased" 8 | "github.com/Equationzhao/g/internal/render" 9 | ) 10 | 11 | type InodeEnabler struct{} 12 | 13 | func NewInodeEnabler() *InodeEnabler { 14 | return &InodeEnabler{} 15 | } 16 | 17 | const Inode = constval.NameOfInode 18 | 19 | func (i *InodeEnabler) Enable(renderer *render.Renderer) ContentOption { 20 | align.RegisterHeaderFooter(Inode) 21 | return func(info *item.FileInfo) (string, string) { 22 | i := "" 23 | if m, ok := info.Cache[Inode]; ok { 24 | i = string(m) 25 | } else { 26 | i = osbased.Inode(info) 27 | } 28 | return renderer.Inode(i), Inode 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/display/item.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | type Content interface { 4 | String() string 5 | } 6 | 7 | type StringContent string 8 | 9 | func (s StringContent) String() string { 10 | return string(s) 11 | } 12 | 13 | type ItemContent struct { 14 | No int 15 | Content Content 16 | Prefix, Suffix string 17 | } 18 | 19 | func (i *ItemContent) SetPrefix(s string) { 20 | i.Prefix = s 21 | } 22 | 23 | func (i *ItemContent) SetSuffix(s string) { 24 | i.Suffix = s 25 | } 26 | 27 | func (i *ItemContent) AddPrefix(add string) { 28 | i.Prefix = add + i.Prefix 29 | } 30 | 31 | func (i *ItemContent) AddSuffix(add string) { 32 | i.Suffix = add + i.Suffix 33 | } 34 | 35 | func (i *ItemContent) String() string { 36 | return i.Prefix + i.Content.String() + i.Suffix 37 | } 38 | 39 | func (i *ItemContent) NO() int { 40 | return i.No 41 | } 42 | -------------------------------------------------------------------------------- /internal/theme/defaultini.go: -------------------------------------------------------------------------------- 1 | //go:build theme 2 | 3 | package theme 4 | 5 | import ( 6 | "encoding/json" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func (s *Style) ToReadable() Style { 12 | r := *s 13 | r.Color = color2str(r.Color) 14 | return r 15 | } 16 | 17 | func init() { 18 | convert := func(theme Theme) { 19 | for k, style := range theme { 20 | theme[k] = style.ToReadable() 21 | } 22 | } 23 | DefaultAll.Apply(convert) 24 | DefaultAll.CheckLowerCase() 25 | marshal, err := json.MarshalIndent(DefaultAll, "", " ") 26 | if err != nil { 27 | panic(err) 28 | } 29 | err = os.WriteFile(filepath.Join("internal", "theme", "default.json"), marshal, 0o644) 30 | if err != nil { 31 | panic(err) 32 | } 33 | err = os.WriteFile(filepath.Join("internal", "theme", "custom_builtin.json"), marshal, 0o644) 34 | if err != nil { 35 | panic(err) 36 | } 37 | os.Exit(0) 38 | } 39 | -------------------------------------------------------------------------------- /internal/osbased/filedetail_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package osbased 4 | 5 | import ( 6 | "os" 7 | "strconv" 8 | "syscall" 9 | ) 10 | 11 | func Inode(info os.FileInfo) string { 12 | stat, ok := info.Sys().(*syscall.Stat_t) 13 | if ok { 14 | return strconv.FormatUint(stat.Ino, 10) 15 | } 16 | return "" 17 | } 18 | 19 | func LinkCount(info os.FileInfo) uint64 { 20 | stat, ok := info.Sys().(*syscall.Stat_t) 21 | if ok { 22 | return uint64(stat.Nlink) 23 | } 24 | return 0 25 | } 26 | 27 | func BlockSize(info os.FileInfo) int64 { 28 | stat, ok := info.Sys().(*syscall.Stat_t) 29 | if !ok { 30 | return 0 31 | } 32 | 33 | return stat.Blocks 34 | } 35 | 36 | // always false on Linux 37 | func IsMacOSAlias(_ string) bool { 38 | return false 39 | } 40 | 41 | // ResolveAlias is a no-op on Linux. 42 | func ResolveAlias(_ string) (string, error) { 43 | return "", nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/content/mounts.go: -------------------------------------------------------------------------------- 1 | //go:build mounts 2 | 3 | package content 4 | 5 | import ( 6 | "github.com/Equationzhao/g/internal/util" 7 | "github.com/shirou/gopsutil/v3/disk" 8 | "github.com/valyala/bytebufferpool" 9 | ) 10 | 11 | func MountsOn(path string) string { 12 | err := mountsOnce.Do(func() error { 13 | mount, err := disk.Partitions(true) 14 | if err != nil { 15 | return err 16 | } 17 | mounts = mount 18 | return nil 19 | }) 20 | if err != nil { 21 | return "" 22 | } 23 | b := bytebufferpool.Get() 24 | defer bytebufferpool.Put(b) 25 | for _, stat := range mounts { 26 | if stat.Mountpoint == path { 27 | _ = b.WriteByte('[') 28 | _, _ = b.WriteString(stat.Device) 29 | _, _ = b.WriteString(" (") 30 | _, _ = b.WriteString(stat.Fstype) 31 | _, _ = b.WriteString(")]") 32 | return b.String() 33 | } 34 | } 35 | return "" 36 | } 37 | 38 | var ( 39 | mounts = make([]disk.PartitionStat, 10) 40 | mountsOnce = util.Once{} 41 | ) 42 | -------------------------------------------------------------------------------- /internal/theme/colorless_test.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import "testing" 4 | 5 | func TestSetClassic(t *testing.T) { 6 | SetClassic() 7 | checker := func(m Theme) { 8 | for k := range m { 9 | if m[k].Color != "" { 10 | t.Errorf("SetClassic() failed, got color %v, want %v", m[k].Color, "") 11 | } 12 | if m[k].Underline != false { 13 | t.Errorf("SetClassic() failed, got underline %v, want %v", m[k].Underline, false) 14 | } 15 | if m[k].Bold != false { 16 | t.Errorf("SetClassic() failed, got bold %v, want %v", m[k].Bold, false) 17 | } 18 | if m[k].Faint != false { 19 | t.Errorf("SetClassic() failed, got faint %v, want %v", m[k].Faint, false) 20 | } 21 | if m[k].Italics != false { 22 | t.Errorf("SetClassic() failed, got italics %v, want %v", m[k].Italics, false) 23 | } 24 | if m[k].Blink != false { 25 | t.Errorf("SetClassic() failed, got blink %v, want %v", m[k].Blink, false) 26 | } 27 | } 28 | } 29 | DefaultAll.Apply(checker) 30 | } 31 | -------------------------------------------------------------------------------- /internal/util/safeSlice.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "sync" 4 | 5 | type Slice[T any] struct { 6 | data []T 7 | m sync.RWMutex 8 | } 9 | 10 | func (s *Slice[T]) Clear() { 11 | s.m.Lock() 12 | s.data = s.data[:0] 13 | s.m.Unlock() 14 | } 15 | 16 | func (s *Slice[T]) AppendTo(d T) { 17 | s.m.Lock() 18 | s.data = append(s.data, d) 19 | s.m.Unlock() 20 | } 21 | 22 | func (s *Slice[T]) GetRaw() *[]T { 23 | return &s.data 24 | } 25 | 26 | func (s *Slice[T]) GetCopy() []T { 27 | s.m.RLock() 28 | defer s.m.RUnlock() 29 | copied := make([]T, len(s.data)) 30 | copy(copied, s.data) 31 | return copied 32 | } 33 | 34 | func (s *Slice[T]) At(pos int) T { 35 | s.m.RLock() 36 | defer s.m.RUnlock() 37 | return s.data[pos] 38 | } 39 | 40 | func (s *Slice[T]) Len() int { 41 | s.m.RLock() 42 | defer s.m.RUnlock() 43 | return len(s.data) 44 | } 45 | 46 | func (s *Slice[T]) Set(pos int, d T) { 47 | s.m.Lock() 48 | defer s.m.Unlock() 49 | s.data[pos] = d 50 | } 51 | 52 | func NewSlice[T any](size int) *Slice[T] { 53 | s := &Slice[T]{ 54 | data: make([]T, 0, size), 55 | } 56 | return s 57 | } 58 | -------------------------------------------------------------------------------- /internal/content/permission.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/Equationzhao/g/internal/align" 7 | constval "github.com/Equationzhao/g/internal/global" 8 | "github.com/Equationzhao/g/internal/item" 9 | "github.com/Equationzhao/g/internal/render" 10 | "github.com/pkg/xattr" 11 | ) 12 | 13 | const Permissions = constval.NameOfPermission 14 | 15 | // EnableFileMode return file mode like -rwxrwxrwx/drwxrwxrwx 16 | func EnableFileMode(renderer *render.Renderer) ContentOption { 17 | align.Register(Permissions) 18 | return func(info *item.FileInfo) (string, string) { 19 | perm := renderer.FileMode(info.Mode().String()) 20 | list, _ := xattr.LList(info.FullPath) 21 | if len(list) != 0 { 22 | perm += "@" 23 | } 24 | return perm, Permissions 25 | } 26 | } 27 | 28 | const OctalPermissions = "Octal" 29 | 30 | func EnableFileOctalPermissions(renderer *render.Renderer) ContentOption { 31 | return func(info *item.FileInfo) (string, string) { 32 | return renderer.OctalPerm( 33 | "0" + strconv.FormatUint(uint64(info.Mode().Perm()), 8), 34 | ), OctalPermissions 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Equationzhao 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 | -------------------------------------------------------------------------------- /docs/Theme.md: -------------------------------------------------------------------------------- 1 | ## create custom theme 2 | 3 | example: [default theme](../internal/theme/default.json) 4 | 5 | ## apply your theme 6 | 7 | ### through command line flag 8 | ```bash 9 | g -theme=path/to/theme [other options] path 10 | ``` 11 | 12 | ### set in the config file 13 | add `Theme: $location` to your profile. `$location` can be found by `g --help` like this: 14 | 15 | ```bash 16 | > g --help 17 | # ... 18 | CONFIG: 19 | Configuration: /Users/equationzhao/Library/Application Support/g/g.yaml 20 | See More at: g.equationzhao.space 21 | # ... 22 | ``` 23 | 24 | here is an example of setting custom theme in the config file: 25 | 26 | ```yaml 27 | Args: 28 | - hyperlink=never 29 | - icons 30 | - disable-index 31 | 32 | CustomTreeStyle: 33 | Child: "├── " 34 | LastChild: "╰── " 35 | Mid: "│ " 36 | Empty: " " 37 | 38 | Theme: /Users/equationzhao/g/internal/theme/your_custom_theme.json 39 | ``` 40 | 41 | ## advanced 42 | 43 | ### build with custom theme 44 | 45 | the custom theme must be placed in the theme directory and named custom_builtin.json (theme/custom_builtin.json) 46 | 47 | ```bash 48 | go build -tags=custom . 49 | ``` 50 | -------------------------------------------------------------------------------- /internal/shell/integration.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "fmt" 7 | 8 | constval "github.com/Equationzhao/g/internal/global" 9 | ) 10 | 11 | //go:embed g.ps1 12 | var PSContent []byte 13 | 14 | //go:embed g.bash 15 | var BASHContent []byte 16 | 17 | //go:embed g.zsh 18 | var ZSHContent []byte 19 | 20 | //go:embed g.fish 21 | var FISHContent []byte 22 | 23 | //go:embed g.nu 24 | var NUContent []byte 25 | 26 | func Init(shell string) (string, error) { 27 | switch shell { 28 | case "zsh": 29 | return string(bytes.ReplaceAll(ZSHContent, []byte("\r\n"), []byte("\n"))), nil 30 | case "bash": 31 | return string(bytes.ReplaceAll(BASHContent, []byte("\r\n"), []byte("\n"))), nil 32 | case "fish": 33 | return string(bytes.ReplaceAll(FISHContent, []byte("\r\n"), []byte("\n"))), nil 34 | case "powershell", "pwsh": 35 | return string(bytes.ReplaceAll(PSContent, []byte("\r\n"), []byte("\n"))), nil 36 | case "nushell", "nu": 37 | return string(bytes.ReplaceAll(NUContent, []byte("\r\n"), []byte("\n"))), nil 38 | // replace os newline with unix newline 39 | } 40 | return "", fmt.Errorf("unsupported shell: %s \n %s[zsh|bash|fish|powershell|nushell]", shell, constval.Success) 41 | } 42 | -------------------------------------------------------------------------------- /internal/index/pathindex_lite.go: -------------------------------------------------------------------------------- 1 | //go:build !fuzzy 2 | 3 | package index 4 | 5 | import "errors" 6 | 7 | // Lite version without fuzzy search dependencies 8 | // This significantly reduces binary size by removing goleveldb and fuzzy dependencies 9 | 10 | var ErrNotSupported = errors.New("fuzzy search feature not available in this build (built without 'fuzzy' tag)") 11 | 12 | func Close() error { 13 | return nil // No-op in lite build 14 | } 15 | 16 | type ErrUpdate struct { 17 | key string 18 | } 19 | 20 | func (e ErrUpdate) Error() string { 21 | return "index update not supported in lite build: " + e.key 22 | } 23 | 24 | func Update(key string) error { 25 | return nil // No-op in lite build - don't return error to avoid breaking basic functionality 26 | } 27 | 28 | func Delete(key string) error { 29 | return nil // No-op in lite build 30 | } 31 | 32 | func RebuildIndex() error { 33 | return nil // No-op in lite build 34 | } 35 | 36 | func All() ([]string, error) { 37 | return nil, ErrNotSupported 38 | } 39 | 40 | func DeleteThose(keys ...string) error { 41 | return nil // No-op in lite build 42 | } 43 | 44 | func FuzzySearch(key string) (string, error) { 45 | // In lite build, just return the original key without fuzzy matching 46 | return key, nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/content/owner.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/Equationzhao/g/internal/align" 7 | 8 | constval "github.com/Equationzhao/g/internal/global" 9 | "github.com/Equationzhao/g/internal/item" 10 | "github.com/Equationzhao/g/internal/osbased" 11 | "github.com/Equationzhao/g/internal/render" 12 | ) 13 | 14 | const ( 15 | OwnerName = constval.NameOfOwner 16 | OwnerUidName = constval.NameOfOwnerUid 17 | OwnerSID = constval.NameOfOwnerSID 18 | ) 19 | 20 | type OwnerEnabler struct { 21 | Numeric bool 22 | } 23 | 24 | func NewOwnerEnabler() *OwnerEnabler { 25 | return &OwnerEnabler{} 26 | } 27 | 28 | func (o *OwnerEnabler) EnableNumeric() { 29 | o.Numeric = true 30 | } 31 | 32 | func (o *OwnerEnabler) DisableNumeric() { 33 | o.Numeric = false 34 | } 35 | 36 | func (o *OwnerEnabler) EnableOwner(renderer *render.Renderer) ContentOption { 37 | align.RegisterHeaderFooter(OwnerName) 38 | return func(info *item.FileInfo) (string, string) { 39 | name, returnFuncName := "", "" 40 | if o.Numeric { 41 | name = osbased.OwnerID(info) 42 | if runtime.GOOS == "windows" { 43 | returnFuncName = OwnerSID 44 | } else { 45 | returnFuncName = OwnerUidName 46 | } 47 | } else { 48 | name = osbased.Owner(info) 49 | returnFuncName = OwnerName 50 | } 51 | return renderer.Owner(name), returnFuncName 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /docs/TestWorkflow.md: -------------------------------------------------------------------------------- 1 | # [Deprecated] Test Workflow 2 | 3 | ## requirement 4 | 5 | the tests directory should be updated with `git submodule update --remote`. 6 | 7 | > ⚠️ there are some test files whose name contains invalid characters in Windows, so the tests can't be updated in Windows. 8 | 9 | the test data contains a series of testfile/dir(under test_data dir), scripts(*.sh), and expected outputs(*.stdout). The scripts are used to generate the outputs, and the expected outputs are used to compare with the outputs. 10 | 11 | make sure you have `just` installed. 12 | 13 | ## pass script/run_test.sh(just test) 14 | `just test` 15 | 16 | check internal/theme/theme_test.go TestAll and TestColor 17 | 18 | > make sure running in a terminal supporting those features 19 | 20 | ## steps to add test case 21 | 22 | ### test flag 23 | 24 | run `just newtest`, and follow the instructions 25 | 26 | example: 27 | ```zsh 28 | > just newtest 29 | test_name: zero 30 | flag: --zero 31 | use base_flag? [Y/n] Y 32 | ``` 33 | 34 | the generated script will be `tests/zero.sh`: 35 | ```sh 36 | output="$(g --no-update -term-width 200 --no-config --icons --permission --size --group --owner --zero tests/test_data )" 37 | echo "$output" | diff - tests/zero.stdout 38 | ``` 39 | 40 | and the output will be zero.stdout 41 | 42 | ### test data 43 | 44 | create files/directories in `tests/test_data` 45 | 46 | run `just reproducetest` to generate the expected output 47 | 48 | 49 | -------------------------------------------------------------------------------- /internal/osbased/flags_darwin.go: -------------------------------------------------------------------------------- 1 | package osbased 2 | 3 | import ( 4 | "os" 5 | "slices" 6 | "syscall" 7 | 8 | "github.com/Equationzhao/g/internal/item" 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | var flags = map[int]string{ 13 | unix.UF_APPEND: "uappnd", 14 | unix.UF_COMPRESSED: "compressed", 15 | unix.UF_HIDDEN: "hidden", 16 | unix.UF_IMMUTABLE: "uchg", 17 | unix.UF_NODUMP: "nodump", 18 | unix.UF_OPAQUE: "opaque", 19 | 20 | // unix.UF_SETTABLE: "UF_SETTABLE", 21 | // unix.UF_TRACKED: "UF_TRACKED", 22 | // unix.UF_DATAVAULT: "UF_DATAVAULT", 23 | 24 | unix.SF_APPEND: "sappnd", 25 | unix.SF_ARCHIVED: "arch", 26 | unix.SF_DATALESS: "dataless", 27 | unix.SF_IMMUTABLE: "schg", 28 | unix.SF_RESTRICTED: "restricted", 29 | 30 | // unix.SF_SETTABLE: "SF_SETTABLE", 31 | // unix.SF_SUPPORTED: "SF_SUPPORTED", 32 | // unix.SF_SYNTHETIC: "SF_SYNTHETIC", 33 | // unix.SF_FIRMLINK: "SF_FIRMLINK", 34 | } 35 | 36 | func getFlags(filename string) uint32 { 37 | file, err := os.Open(filename) 38 | if err != nil { 39 | return 0 40 | } 41 | defer file.Close() 42 | 43 | fileInfo, err := file.Stat() 44 | if err != nil { 45 | return 0 46 | } 47 | 48 | stat := fileInfo.Sys().(*syscall.Stat_t) 49 | return stat.Flags 50 | } 51 | 52 | func CheckFlags(i *item.FileInfo) []string { 53 | res := make([]string, 0, 8) 54 | f := getFlags(i.FullPath) 55 | for key, val := range flags { 56 | if f&uint32(key) != 0 { 57 | res = append(res, val) 58 | } 59 | } 60 | slices.Sort(res) 61 | return res 62 | } 63 | -------------------------------------------------------------------------------- /internal/content/size_test.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseSize(t *testing.T) { 8 | testHelper := func(sizeStr string, sizeAmount float64, unit SizeUnit) { 9 | size, err := ParseSize(sizeStr) 10 | if err != nil { 11 | t.Errorf("%s:ParseSize() error = %v", sizeStr, err) 12 | } 13 | expected := uint64(sizeAmount * float64(CountBytes(unit))) 14 | if size.Bytes != expected { 15 | t.Errorf("%s:ParseSize() got = %v, want %v", sizeStr, size.Bytes, expected) 16 | } 17 | } 18 | 19 | _, err := ParseSize("unknown") 20 | // todo define error struct 21 | t.Log(err) 22 | 23 | _, err = ParseSize("-1kb") 24 | t.Log(err) 25 | 26 | testHelper("123byte", 123, Byte) 27 | testHelper("1,234.5KB", 1_234.5, KB) 28 | testHelper("1,234.5KiB", 1_234.5, KiB) 29 | testHelper("1,234.5K", 1_234.5, KB) 30 | testHelper("1,234.5k", 1_234.5, KB) 31 | testHelper("1,234.5kb", 1_234.5, KB) 32 | testHelper("4.5GB", 4.5, GB) 33 | testHelper("4.5GiB", 4.5, GiB) 34 | testHelper("4.5G", 4.5, GB) 35 | testHelper("4.5g", 4.5, GB) 36 | testHelper("4.5gb", 4.5, GB) 37 | testHelper("200MB", 200, MB) 38 | testHelper("200MiB", 200, MiB) 39 | testHelper("200M", 200, MB) 40 | testHelper("200mb", 200, MB) 41 | testHelper("200m", 200, MB) 42 | testHelper("3,123,432.321TB", 3_123_432.321, TB) 43 | testHelper("3,123,432.321TiB", 3_123_432.321, TiB) 44 | testHelper("3,123,432.321T", 3_123_432.321, TB) 45 | testHelper("3,123,432.321tb", 3_123_432.321, TB) 46 | testHelper("3,123,432.321t", 3_123_432.321, TB) 47 | } 48 | -------------------------------------------------------------------------------- /scoop/g.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "g.equationzhao.space", 3 | "bin": "bin/g.exe", 4 | "architecture": { 5 | "64bit": { 6 | "url": "https://github.com/Equationzhao/g/releases/download/v0.31.0/g-windows-amd64.exe", 7 | "hash": "d31d392257fc8bf0a1138ba325828107d28113637ca2b467ec4b01b96ae8b4b8", 8 | "bin": "g-windows-amd64.exe", 9 | "post_install": [ 10 | "cd $scoopdir/shims", 11 | "mv g-windows-amd64.exe g.exe", 12 | "mv g-windows-amd64.shim g.shim" 13 | ], 14 | "shortcuts": [["g-windows-amd64.exe", "g"]] 15 | }, 16 | "32bit": { 17 | "url": "https://github.com/Equationzhao/g/releases/download/v0.31.0/g-windows-386.exe", 18 | "hash": "2247dbf1aab7999bef64fe9a382cd12fadc0d38f8d1c52398e239b0fdcc685ab", 19 | "bin": "g-windows-386.exe", 20 | "post_install": [ 21 | "cd $scoopdir/shims", 22 | "mv g-windows-386.exe g.exe", 23 | "mv g-windows-386.shim g.shim" 24 | ], 25 | "shortcuts": [["g-windows-386.exe", "g"]] 26 | }, 27 | "arm64": { 28 | "url": "https://github.com/Equationzhao/g/releases/download/v0.31.0/g-windows-arm64.exe", 29 | "hash": "6b69187990d650a8630ce71588b4c351e93c0e5d5ac81f1b027ec91c1abd6d85", 30 | "bin": "g-windows-arm64.exe", 31 | "post_install": [ 32 | "cd $scoopdir/shims", 33 | "mv g-windows-arm64.exe g.exe", 34 | "mv g-windows-arm64.shim g.shim" 35 | ], 36 | "shortcuts": [["g-windows-arm64.exe", "g"]] 37 | } 38 | }, 39 | "license": "MIT", 40 | "version": "v0.31.0" 41 | } 42 | -------------------------------------------------------------------------------- /internal/osbased/filedetail_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package osbased 4 | 5 | /* 6 | #cgo CFLAGS: -mmacosx-version-min=10.9 7 | #cgo LDFLAGS: -framework CoreFoundation -framework CoreServices 8 | #include "macos_alias.h" 9 | */ 10 | import "C" 11 | 12 | import ( 13 | "fmt" 14 | "os" 15 | "strconv" 16 | "syscall" 17 | "unsafe" 18 | ) 19 | 20 | func Inode(info os.FileInfo) string { 21 | stat, ok := info.Sys().(*syscall.Stat_t) 22 | if ok { 23 | return strconv.FormatUint(stat.Ino, 10) 24 | } 25 | return "" 26 | } 27 | 28 | func LinkCount(info os.FileInfo) uint64 { 29 | stat, ok := info.Sys().(*syscall.Stat_t) 30 | if ok { 31 | return uint64(stat.Nlink) 32 | } 33 | return 0 34 | } 35 | 36 | func BlockSize(info os.FileInfo) int64 { 37 | stat, ok := info.Sys().(*syscall.Stat_t) 38 | if !ok { 39 | return 0 40 | } 41 | 42 | return stat.Blocks 43 | } 44 | 45 | func IsMacOSAlias(fullPath string) bool { 46 | fi, err := os.Lstat(fullPath) 47 | if err != nil { 48 | return false 49 | } 50 | 51 | if fi.Mode()&os.ModeSymlink != 0 { 52 | return false 53 | } 54 | 55 | cPath := C.CString(fullPath) 56 | defer C.free(unsafe.Pointer(cPath)) 57 | return bool(C.isAlias(cPath)) 58 | } 59 | 60 | func ResolveAlias(fullPath string) (string, error) { 61 | cPath := C.CString(fullPath) 62 | defer C.free(unsafe.Pointer(cPath)) 63 | 64 | resolved := C.resolveAlias(cPath) 65 | if resolved == nil { 66 | return "", fmt.Errorf("failed to resolve macOS alias for %s", fullPath) 67 | } 68 | defer C.free(unsafe.Pointer(resolved)) 69 | 70 | return C.GoString(resolved), nil 71 | } 72 | -------------------------------------------------------------------------------- /script/new_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This script is deprecated" 4 | exit 0 5 | 6 | # creat a new test file in tests/*.sh and its output in tests/*.stdout 7 | 8 | # load base.sh 9 | source "$(dirname "$0")/base.sh" 10 | 11 | if [ ! -f "script/run_test.sh" ]; then 12 | error "Please run the script in the root directory of the project" 13 | exit 1 14 | fi 15 | 16 | # input test_name 17 | read -p "test_name: " test_name 18 | 19 | if [ -z "$test_name" ]; then 20 | error "test_name is empty" 21 | exit 1 22 | fi 23 | 24 | read -p "flag: " flag 25 | if [ -z "$flag" ]; then 26 | error "flag is empty" 27 | exit 1 28 | fi 29 | 30 | command="g" 31 | forever_base="--no-update" 32 | base_flag="-term-width 200 --no-config --icons --permission --size" 33 | 34 | read -p "use base_flag? [Y/n] " -n 1 -r 35 | echo 36 | if [[ $REPLY =~ ^[Nn]$ ]]; then 37 | base_flag="" 38 | fi 39 | 40 | running_command="$command $forever_base $base_flag $flag" 41 | 42 | test_script="tests/$test_name.sh" 43 | test_stdout="tests/$test_name.stdout" 44 | 45 | if [ -f "$test_script" ]; then 46 | warn "$test_script already exists" 47 | read -p "Do you want to overwrite it? [y/N] " -n 1 -r 48 | echo 49 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 50 | exit 1 51 | fi 52 | fi 53 | 54 | output="$($running_command tests/test_data)" 55 | 56 | echo "$output" > $test_stdout 57 | echo "#!/bin/bash" > "$test_script" 58 | echo "output=\"\$($running_command tests/test_data )\"" >> "$test_script" 59 | echo "echo \"\$output\" | diff - $test_stdout" >> "$test_script" 60 | 61 | chmod +x "$test_script" 62 | -------------------------------------------------------------------------------- /internal/content/group.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/Equationzhao/g/internal/align" 7 | 8 | constval "github.com/Equationzhao/g/internal/global" 9 | "github.com/Equationzhao/g/internal/item" 10 | "github.com/Equationzhao/g/internal/osbased" 11 | "github.com/Equationzhao/g/internal/render" 12 | ) 13 | 14 | type GroupEnabler struct { 15 | Numeric bool 16 | Smart bool 17 | } 18 | 19 | const ( 20 | GroupName = constval.NameOfGroupName 21 | GroupUidName = constval.NameOfGroupUidName 22 | GroupSID = constval.NameOfGroupSID 23 | ) 24 | 25 | func NewGroupEnabler() *GroupEnabler { 26 | return &GroupEnabler{} 27 | } 28 | 29 | func (g *GroupEnabler) EnableNumeric() { 30 | g.Numeric = true 31 | } 32 | 33 | func (g *GroupEnabler) DisableNumeric() { 34 | g.Numeric = false 35 | } 36 | 37 | func (g *GroupEnabler) EnableSmartMode() { 38 | g.Smart = true 39 | } 40 | 41 | func (g *GroupEnabler) DisableSmartMode() { 42 | g.Smart = false 43 | } 44 | 45 | func (g *GroupEnabler) EnableGroup(renderer *render.Renderer) ContentOption { 46 | align.RegisterHeaderFooter(GroupName) 47 | return func(info *item.FileInfo) (string, string) { 48 | name, returnFuncName := "", GroupName 49 | if g.Numeric { 50 | name = osbased.GroupID(info) 51 | if runtime.GOOS == "windows" { 52 | returnFuncName = GroupSID 53 | } else { 54 | returnFuncName = GroupUidName 55 | } 56 | } else { 57 | name = osbased.Group(info) 58 | if g.Smart && name == osbased.Owner(info) { 59 | name = "" 60 | } 61 | } 62 | return renderer.Group(name), returnFuncName 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/cached/cached.go: -------------------------------------------------------------------------------- 1 | package cached 2 | 3 | import ( 4 | constval "github.com/Equationzhao/g/internal/global" 5 | "github.com/alphadose/haxmap" 6 | ) 7 | 8 | type Map[k constval.Hashable, v any] struct { 9 | *haxmap.Map[k, v] 10 | } 11 | 12 | func NewCacheMap[k constval.Hashable, v any](len int) *Map[k, v] { 13 | return &Map[k, v]{ 14 | haxmap.New[k, v](uintptr(len)), 15 | } 16 | } 17 | 18 | func (m Map[k, v]) Keys() []k { 19 | keys := make([]k, 0, m.Len()) 20 | m.ForEach(func(k k, v v) bool { 21 | keys = append(keys, k) 22 | return true 23 | }) 24 | return keys 25 | } 26 | 27 | func (m Map[k, v]) Values() []v { 28 | values := make([]v, 0, m.Len()) 29 | m.ForEach(func(k k, v v) bool { 30 | values = append(values, v) 31 | return true 32 | }) 33 | return values 34 | } 35 | 36 | func (m Map[k, v]) Pairs() []Pair[k, v] { 37 | pairs := make([]Pair[k, v], 0, m.Len()) 38 | m.ForEach(func(key k, value v) bool { 39 | pairs = append(pairs, Pair[k, v]{ 40 | First: key, 41 | Second: value, 42 | }) 43 | return true 44 | }) 45 | return pairs 46 | } 47 | 48 | // Pair is a struct that contains two variables ptr 49 | type Pair[T, U any] struct { 50 | First T 51 | Second U 52 | } 53 | 54 | // MakePair return a new Pair 55 | // receive two value 56 | func MakePair[T, U any](first T, second U) Pair[T, U] { 57 | return Pair[T, U]{ 58 | First: first, 59 | Second: second, 60 | } 61 | } 62 | 63 | // Set the pair 64 | // Copy the `first` and `second` to the pair 65 | func (p *Pair[T, U]) Set(first T, second U) { 66 | p.First = first 67 | p.Second = second 68 | } 69 | 70 | func (p Pair[T, U]) Key() T { 71 | return p.First 72 | } 73 | 74 | func (p Pair[T, U]) Value() U { 75 | return p.Second 76 | } 77 | -------------------------------------------------------------------------------- /internal/util/safeSlice_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | ) 7 | 8 | func TestSlice_AppendTo(t *testing.T) { 9 | s := NewSlice[int](10) 10 | s.AppendTo(1) 11 | s.AppendTo(2) 12 | s.AppendTo(3) 13 | if s.Len() != 3 { 14 | t.Errorf("AppendTo failed") 15 | } 16 | } 17 | 18 | func TestSlice_At(t *testing.T) { 19 | s := Slice[int]{ 20 | data: []int{1, 2, 3}, 21 | m: sync.RWMutex{}, 22 | } 23 | if s.At(1) != 2 { 24 | t.Errorf("At failed") 25 | } 26 | } 27 | 28 | func TestSlice_Clear(t *testing.T) { 29 | s := Slice[int]{ 30 | data: []int{1, 2, 3}, 31 | m: sync.RWMutex{}, 32 | } 33 | s.Clear() 34 | if s.Len() != 0 { 35 | t.Errorf("Clear failed") 36 | } 37 | } 38 | 39 | func TestSlice_GetRaw(t *testing.T) { 40 | s := Slice[int]{ 41 | data: []int{1, 2, 3}, 42 | m: sync.RWMutex{}, 43 | } 44 | gotRaw := s.GetRaw() 45 | if len(*gotRaw) != 3 { 46 | t.Errorf("GetRaw failed") 47 | } 48 | if gotRaw != &s.data { 49 | t.Errorf("GetRaw failed") 50 | } 51 | } 52 | 53 | func TestSlice_GetCopy(t *testing.T) { 54 | s := Slice[int]{ 55 | data: []int{1, 2, 3}, 56 | m: sync.RWMutex{}, 57 | } 58 | copied := s.GetCopy() 59 | if len(copied) != 3 { 60 | t.Errorf("GetCopy failed") 61 | } 62 | if &copied == &s.data { 63 | t.Errorf("GetCopy failed") 64 | } 65 | } 66 | 67 | func TestSlice_Len(t *testing.T) { 68 | s := Slice[int]{ 69 | data: []int{1, 2, 3}, 70 | m: sync.RWMutex{}, 71 | } 72 | if s.Len() != 3 { 73 | t.Errorf("Len failed") 74 | } 75 | } 76 | 77 | func TestSlice_Set(t *testing.T) { 78 | s := Slice[int]{ 79 | data: []int{1, 2, 3}, 80 | m: sync.RWMutex{}, 81 | } 82 | s.Set(1, 4) 83 | if s.At(1) != 4 { 84 | t.Errorf("Set failed") 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /internal/cached/cached_test.go: -------------------------------------------------------------------------------- 1 | package cached 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zeebo/assert" 7 | ) 8 | 9 | func TestNewCacheMap(t *testing.T) { 10 | cacheMap := NewCacheMap[int, int](10) 11 | if cacheMap == nil { 12 | t.Error("NewCacheMap failed") 13 | } 14 | } 15 | 16 | func TestMap_Keys(t *testing.T) { 17 | cacheMap := NewCacheMap[int, int](10) 18 | cacheMap.Set(1, 2) 19 | cacheMap.Set(2, 3) 20 | keys := cacheMap.Keys() 21 | if len(keys) != 2 { 22 | t.Error("Keys failed") 23 | } 24 | assert.DeepEqual(t, keys, []int{1, 2}) 25 | } 26 | 27 | func TestMap_Values(t *testing.T) { 28 | cacheMap := NewCacheMap[int, int](10) 29 | cacheMap.Set(1, 2) 30 | cacheMap.Set(2, 3) 31 | values := cacheMap.Values() 32 | if len(values) != 2 { 33 | t.Error("Values failed") 34 | } 35 | assert.DeepEqual(t, values, []int{2, 3}) 36 | } 37 | 38 | func TestMap_Pairs(t *testing.T) { 39 | cacheMap := NewCacheMap[int, int](10) 40 | cacheMap.Set(1, 2) 41 | cacheMap.Set(2, 3) 42 | pairs := cacheMap.Pairs() 43 | if len(pairs) != 2 { 44 | t.Error("Pairs failed") 45 | } 46 | assert.DeepEqual(t, pairs, []Pair[int, int]{{First: 1, Second: 2}, {First: 2, Second: 3}}) 47 | } 48 | 49 | func TestMakePair(t *testing.T) { 50 | pair := MakePair(1, 2) 51 | if pair.First != 1 || pair.Second != 2 { 52 | t.Error("MakePair failed") 53 | } 54 | } 55 | 56 | func TestPair(t *testing.T) { 57 | pair := Pair[int, int]{} 58 | if pair.First != 0 || pair.Second != 0 { 59 | t.Error("Pair failed") 60 | } 61 | 62 | pair.Set(1, 2) 63 | if pair.First != 1 || pair.Second != 2 { 64 | t.Error("Pair Set failed") 65 | } 66 | 67 | if pair.Key() != 1 { 68 | t.Error("Pair Key failed") 69 | } 70 | 71 | if pair.Value() != 2 { 72 | t.Error("Pair Value failed") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/git/ignoredcache.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/Equationzhao/g/internal/cached" 7 | "github.com/Equationzhao/pathbeautify" 8 | "github.com/zeebo/xxh3" 9 | ) 10 | 11 | var ( 12 | ignored *cached.Map[RepoPath, *FileGits] 13 | IgnoredInitOnce sync.Once 14 | TopLevelCache *cached.Map[RepoPath, RepoPath] 15 | TopLevelInitOnce sync.Once 16 | ) 17 | 18 | const size = 20 19 | 20 | func hasher(s string) uintptr { 21 | return uintptr(xxh3.HashString(s)) 22 | } 23 | 24 | type Cache = *cached.Map[RepoPath, *FileGits] 25 | 26 | func GetCache() Cache { 27 | IgnoredInitOnce.Do( 28 | func() { 29 | ignored = cached.NewCacheMap[RepoPath, *FileGits](size) 30 | ignored.SetHasher(hasher) 31 | }, 32 | ) 33 | return ignored 34 | } 35 | 36 | func DefaultInit(repoPath RepoPath) func() *FileGits { 37 | return func() *FileGits { 38 | res := make(FileGits, 0) 39 | out, err := GetShortGitStatus(repoPath) 40 | if err == nil && out != "" { 41 | res = ParseShort(out) 42 | } 43 | return &res 44 | } 45 | } 46 | 47 | // GetTopLevel returns the top level of the repoPath 48 | // the returned path is cleaned by pathbeautify.CleanSeparator 49 | func GetTopLevel(path string) (RepoPath, error) { 50 | TopLevelInitOnce.Do( 51 | func() { 52 | if TopLevelCache == nil { 53 | TopLevelCache = cached.NewCacheMap[RepoPath, RepoPath](size) 54 | TopLevelCache.SetHasher(hasher) 55 | } 56 | }, 57 | ) 58 | var err error 59 | actual, _ := TopLevelCache.GetOrCompute( 60 | path, func() RepoPath { 61 | out, err_ := getTopLevel(path) 62 | if err_ != nil { 63 | err = err_ 64 | return "" 65 | } 66 | return out 67 | }, 68 | ) 69 | actual = pathbeautify.CleanSeparator(actual) 70 | return actual, err 71 | } 72 | -------------------------------------------------------------------------------- /script/install_dev_requirement.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # install development requirements 3 | 4 | lists=( 5 | "git" 6 | "upx" 7 | "dpkg" 8 | "gh" 9 | "wget" 10 | "gofumpt" 11 | "just" 12 | "prettier" 13 | "choose-rust" 14 | "ripgrep" 15 | "golangci-lint" 16 | ) 17 | 18 | command=( 19 | "git" 20 | "upx" 21 | "dpkg" 22 | "gh" 23 | "wget" 24 | "gofumpt" 25 | "just" 26 | "prettier" 27 | "choose" 28 | "rg" 29 | "golangci-lint" 30 | ) 31 | 32 | # load base.sh 33 | source "$(dirname "$0")/base.sh" 34 | 35 | # check if brew is installed 36 | if ! command -v brew &> /dev/null; then 37 | error "brew is not installed" 38 | exit 1 39 | fi 40 | 41 | echo "brew update" 42 | brew update > /dev/null 43 | if [ $? -ne 0 ]; then 44 | error "brew update failed" 45 | fi 46 | 47 | if ! command -v go &> /dev/null; then 48 | echo "brew install go" 49 | brew install go 50 | else # check go version >= 1.21.0 51 | go_version=$(go version | awk '{print $3}') 52 | go_version=${go_version:2} 53 | if [ "$(printf '%s\n' "1.21.0" "$go_version" | sort -V | head -n1)" != "1.21.0" ]; then 54 | # check if go is installed by brew 55 | if brew list --versions go &> /dev/null; then 56 | echo "brew upgrade go" 57 | brew upgrade go 58 | else 59 | echo "please upgrade go to 1.21.0 or later" 60 | fi 61 | fi 62 | fi 63 | 64 | 65 | for i in "${!lists[@]}"; do 66 | if ! command -v "${command[i]}" &> /dev/null; then 67 | echo "brew install ${lists[i]}..." 68 | HOMEBREW_NO_AUTO_UPDATE=1 brew install "${lists[i]}" > /dev/null 69 | if [ $? -ne 0 ]; then 70 | error "brew install ${lists[i]} failed" 71 | else 72 | success "${lists[i]} installed" 73 | fi 74 | else 75 | success "${lists[i]} already installed" 76 | fi 77 | done -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/Equationzhao/g/internal/global" 10 | ) 11 | 12 | func RemoveSep(s string) string { 13 | return strings.TrimRight(s, string(filepath.Separator)) 14 | } 15 | 16 | var escapeReplacer = strings.NewReplacer( 17 | "\t", reverseColor(`\t`), 18 | "\r", reverseColor(`\r`), 19 | "\n", reverseColor(`\n`), 20 | "\"", reverseColor(`\"`), 21 | "\\", reverseColor(`\\`), 22 | ) 23 | 24 | func reverseColor(s string) string { 25 | return global.Reverse + s + global.ReverseDone 26 | } 27 | 28 | // Escape 29 | // * Tab is escaped as `\t`. 30 | // * Carriage return is escaped as `\r`. 31 | // * Line feed is escaped as `\n`. 32 | // * Single quote is escaped as `\'`. 33 | // * Double quote is escaped as `\"`. 34 | // * Backslash is escaped as `\\`. 35 | func Escape(a string) string { 36 | return escapeReplacer.Replace(a) 37 | } 38 | 39 | func MakeLink(abs, name string) string { 40 | return fmt.Sprintf("\033]8;;%s\033\\%s\033]8;;\033\\", abs, name) 41 | } 42 | 43 | // SplitNumberAndUnit splits a string like 44 | // "10bit" to 10 and "bit" 45 | // 46 | // "12.3ml" to 12.4 and "ml" 47 | // 48 | // "-1,234,213kg" to -1234213 and "kg" 49 | func SplitNumberAndUnit(input string) (float64, string) { 50 | var number float64 51 | var unit string 52 | 53 | // Find the index of the first non-digit character 54 | i := 0 55 | for i < len(input) && (input[i] >= '0' && input[i] <= '9' || input[i] == '.' || input[i] == '-' || input[i] == ',') { 56 | i++ 57 | } 58 | 59 | // Parse the number part 60 | numberPart := input[:i] 61 | number, _ = strconv.ParseFloat(strings.ReplaceAll(numberPart, ",", ""), 64) 62 | 63 | // Extract the unit part 64 | unit = input[i:] 65 | 66 | return number, unit 67 | } 68 | -------------------------------------------------------------------------------- /internal/git/git_commit.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os/exec" 7 | "strings" 8 | "time" 9 | 10 | strftime "github.com/itchyny/timefmt-go" 11 | ) 12 | 13 | type CommitInfo struct { 14 | Hash string `json:"h"` 15 | 16 | Committer string `json:"c"` 17 | CommitterDate string `json:"cd"` 18 | 19 | Author string `json:"a"` 20 | AuthorDate string `json:"ad"` 21 | } 22 | 23 | func (c CommitInfo) GetCommitterDateInFormat(format string) string { 24 | t, err := time.Parse(time.RFC3339, c.CommitterDate) 25 | if err != nil { 26 | return "" 27 | } 28 | return t.Format(format) 29 | } 30 | 31 | func (c CommitInfo) GetAuthorDateInFormat(format string) string { 32 | t, err := time.Parse(goParseFormat, c.AuthorDate) 33 | if err != nil { 34 | return "" 35 | } 36 | if strings.HasPrefix(format, "+") { 37 | return strftime.Format(t, strings.TrimPrefix(format, "+")) 38 | } 39 | return t.Format(format) 40 | } 41 | 42 | var NoneCommitInfo = CommitInfo{"-", "-", "-", "-", "-"} 43 | 44 | // https://github.com/chaqchase/lla/blob/main/plugins/last_git_commit/src/lib.rs 45 | func GetLastCommitInfo(path string) (*CommitInfo, error) { 46 | return getLastCommitInfo(path) 47 | } 48 | 49 | const ( 50 | gitDateFormat = `format:"%Y-%m-%d %H:%M:%S.%9N %z"` 51 | goParseFormat = time.RFC3339 52 | ) 53 | 54 | func getLastCommitInfo(path string) (*CommitInfo, error) { 55 | cmd := exec.Command("git", "log", "-1", `--pretty=format:{"h":"%h","a":"%an","c":"%cn","ad":"%aI","cd":"%cI"}`, fmt.Sprintf(`--date=%s`, gitDateFormat), path) 56 | 57 | output, err := cmd.Output() 58 | if err != nil { 59 | return nil, err 60 | } 61 | if len(output) == 0 { 62 | return &NoneCommitInfo, nil 63 | } 64 | var info CommitInfo 65 | if err := json.Unmarshal(output, &info); err != nil { 66 | return nil, err 67 | } 68 | 69 | return &info, nil 70 | } 71 | -------------------------------------------------------------------------------- /script/run_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This script is deprecated" 4 | exit 0 5 | 6 | # should run in the root directory of the project 7 | # run all tests under tests/*.sh 8 | 9 | if [ ! -f "script/run_test.sh" ]; then 10 | echo "Please run the script in the root directory of the project" 11 | exit 1 12 | fi 13 | 14 | GREEN='\033[0;32m' 15 | RED='\033[0;31m' 16 | NO_COLOR='\033[0m' 17 | 18 | # Directory containing tests 19 | TEST_DIR="tests" 20 | 21 | pass_count=0 22 | fail_count=0 23 | 24 | # run g --help and get Configuration file path 25 | # backup Configuration file 26 | # Configuration: path (may contains space) 27 | config_path=$(g --help | grep "Configuration:" | cut -d ":" -f 2- | sed -e 's/^[[:space:]]*//') 28 | should_restore=0 29 | if [ -f "$config_path" ]; then 30 | echo "disable config: $config_path" 31 | echo "backup config: $config_path.bak" 32 | # if success, restore it at the end 33 | if mv "$config_path" "$config_path.bak"; then 34 | should_restore=1 35 | fi 36 | fi 37 | 38 | 39 | # Run tests 40 | for test_script in "$TEST_DIR"/*.sh; do 41 | # Run the script and capture the output 42 | output=$(bash "$test_script" 2>&1) 43 | 44 | # Check if output is empty 45 | if [ -z "$output" ]; then 46 | # Test passed 47 | echo "${GREEN}Passed:${NO_COLOR} $test_script" 48 | pass_count=$((pass_count+1)) 49 | else 50 | # Test failed 51 | echo 52 | echo "${RED}Failed:${NO_COLOR} $test_script" 53 | echo "${RED}$output${NO_COLOR}" 54 | fail_count=$((fail_count+1)) 55 | fi 56 | done 57 | 58 | echo 59 | echo "Passed: $pass_count" 60 | echo "Failed: $fail_count" 61 | 62 | # Restore Configuration file 63 | if [ "$should_restore" -eq 1 ]; then 64 | echo "restore config: $config_path" 65 | mv "$config_path.bak" "$config_path" 66 | fi 67 | 68 | if [ "$fail_count" -gt 0 ]; then 69 | exit 1 70 | fi -------------------------------------------------------------------------------- /internal/cached/cachedmaps.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin 2 | 3 | package cached 4 | 5 | import ( 6 | "os/user" 7 | ) 8 | 9 | type ( 10 | Uid = string 11 | Username = string 12 | ) 13 | 14 | // usernameMap is a map from Uid to Username 15 | // current not contained because it is cached in user.Current() 16 | type usernameMap struct { 17 | m *Map[Uid, Username] 18 | } 19 | 20 | func NewUsernameMap() *usernameMap { 21 | return &usernameMap{ 22 | m: NewCacheMap[Uid, Username](20), 23 | } 24 | } 25 | 26 | func (m *usernameMap) Get(u Uid) Username { 27 | if c, err := user.Current(); err == nil && c.Uid == u { 28 | return c.Username 29 | } 30 | 31 | v, _ := m.m.GetOrCompute(u, func() Groupname { 32 | targetUser, err := user.LookupId(u) 33 | if err != nil { 34 | if targetUser == nil { 35 | targetUser = new(user.User) 36 | } 37 | targetUser.Username = "uid:" + u 38 | } 39 | return targetUser.Username 40 | }) 41 | return v 42 | } 43 | 44 | type ( 45 | Gid = string 46 | Groupname = string 47 | ) 48 | 49 | // groupnameMap is a map from Gid to Groupname 50 | type groupnameMap struct { 51 | m *Map[Gid, Groupname] 52 | } 53 | 54 | func NewGroupnameMap() *groupnameMap { 55 | return &groupnameMap{ 56 | m: NewCacheMap[Gid, Groupname](20), 57 | } 58 | } 59 | 60 | func (m *groupnameMap) Get(g Gid) Groupname { 61 | v, _ := m.m.GetOrCompute(g, func() Groupname { 62 | targetGroup, err := user.LookupGroupId(g) 63 | if err != nil { 64 | if targetGroup == nil { 65 | targetGroup = new(user.Group) 66 | } 67 | targetGroup.Name = "gid:" + g 68 | } 69 | return targetGroup.Name 70 | }) 71 | return v 72 | } 73 | 74 | var ( 75 | mainGroupnameMap = NewGroupnameMap() 76 | mainUsernameMap = NewUsernameMap() 77 | ) 78 | 79 | func GetGroupname(g Gid) Groupname { 80 | return mainGroupnameMap.Get(g) 81 | } 82 | 83 | func GetUsername(u Uid) Groupname { 84 | return mainUsernameMap.Get(u) 85 | } 86 | -------------------------------------------------------------------------------- /internal/config/load_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/agiledragon/gomonkey/v2" 8 | "github.com/zeebo/assert" 9 | ) 10 | 11 | func TestLoad(t *testing.T) { 12 | p := gomonkey.NewPatches() 13 | p.ApplyFunc(os.UserConfigDir, func() (string, error) { 14 | return "/home/user", nil 15 | }).ApplyFunc(os.MkdirAll, func(path string, perm os.FileMode) error { 16 | return nil 17 | }).ApplyFunc(os.ReadFile, func(name string) ([]byte, error) { 18 | return []byte(`Args: 19 | - hyperlink=never 20 | - icons 21 | - fuzzy 22 | 23 | CustomTreeStyle: 24 | Child: "├── " 25 | LastChild: "╰── " 26 | Mid: "│ " 27 | Empty: " "`), nil 28 | }) 29 | defer p.Reset() 30 | 31 | load, err := Load() 32 | assert.NoError(t, err) 33 | assert.DeepEqual(t, load.Args, []string{"--hyperlink=never", "--icons", "--fuzzy"}) 34 | assert.DeepEqual(t, load.CustomTreeStyle, TreeStyle{ 35 | Child: "├── ", 36 | LastChild: "╰── ", 37 | Mid: "│ ", 38 | Empty: " ", 39 | }) 40 | } 41 | 42 | func TestTreeStyle_IsEnabled(t1 *testing.T) { 43 | type fields struct { 44 | Child string 45 | LastChild string 46 | Mid string 47 | Empty string 48 | } 49 | tests := []struct { 50 | name string 51 | fields fields 52 | want bool 53 | }{ 54 | { 55 | name: "test", 56 | fields: fields{ 57 | Child: "├── ", 58 | LastChild: "╰── ", 59 | Mid: "│ ", 60 | Empty: " ", 61 | }, 62 | want: true, 63 | }, 64 | { 65 | name: "test", 66 | fields: fields{ 67 | Child: "", 68 | LastChild: "", 69 | Mid: "", 70 | Empty: "", 71 | }, 72 | }, 73 | } 74 | for _, tt := range tests { 75 | t1.Run(tt.name, func(t1 *testing.T) { 76 | ts := TreeStyle{ 77 | Child: tt.fields.Child, 78 | LastChild: tt.fields.LastChild, 79 | Mid: tt.fields.Mid, 80 | Empty: tt.fields.Empty, 81 | } 82 | if got := ts.IsEnabled(); got != tt.want { 83 | t1.Errorf("IsEnabled() = %v, want %v", got, tt.want) 84 | } 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /docs/BuildOption.md: -------------------------------------------------------------------------------- 1 | # Build Configuration 2 | 3 | This document describes the optional features that can be enabled or disabled during compilation to control the binary size. 4 | 5 | ## Build Tags 6 | 7 | The `g` CLI tool supports conditional compilation using Go build tags to include or exclude optional features: 8 | 9 | ### `fuzzy` tag 10 | - **Purpose**: Enables fuzzy search and path indexing functionality 11 | - **Dependencies**: `github.com/syndtr/goleveldb`, `github.com/sahilm/fuzzy` 12 | - **Size impact**: ~500KB 13 | - **Features affected**: 14 | - `--fuzzy` flag for fuzzy path searching 15 | - Path indexing and index management commands 16 | - **Usage**: `go build -tags="fuzzy" .` 17 | 18 | ### `mounts` tag 19 | - **Purpose**: Enables mount point detection and display 20 | - **Dependencies**: `github.com/shirou/gopsutil/v3` 21 | - **Size impact**: ~200KB 22 | - **Features affected**: 23 | - `--mounts` flag to show mount details for files 24 | - **Usage**: `go build -tags="mounts" .` 25 | 26 | ## Build Examples 27 | 28 | ### Lite build (minimal size) 29 | ```bash 30 | go build -ldflags="-s -w" -o g-lite . 31 | ``` 32 | - Size: ~7.4MB/7.0MiB for macOS 33 | - Features: Core functionality only (no fuzzy search, no mount info) 34 | 35 | ### Full build (all features) 36 | ```bash 37 | go build -ldflags="-s -w" -tags="fuzzy mounts" -o g-full . 38 | ``` 39 | - Size: ~8.1MB/7.7MiB for macOS 40 | - Features: All optional features enabled 41 | 42 | ### Custom builds 43 | ```bash 44 | # Only fuzzy search 45 | go build -ldflags="-s -w" -tags="fuzzy" -o g-fuzzy . 46 | 47 | # Only mounts 48 | go build -ldflags="-s -w" -tags="mounts" -o g-mounts . 49 | ``` 50 | 51 | ## Behavior without optional features 52 | 53 | ### Without `fuzzy` tag: 54 | - `--fuzzy` flag is silently ignored (no error) 55 | - No path indexing occurs 56 | - Fuzzy path matching falls back to exact path matching 57 | 58 | ### Without `mounts` tag: 59 | - `--mounts` flag is silently ignored (no mount info displayed) 60 | - No system partition scanning occurs 61 | 62 | This approach allows users to choose between a smaller binary size and optional functionality based on their needs. -------------------------------------------------------------------------------- /scoop/scoop.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | latest=$(git describe --abbrev=0 --tags | sed 's/v//') 4 | hash64=$(shasum -a 256 ../build/g-windows-amd64.exe | cut -d ' ' -f 1) 5 | hash386=$(shasum -a 256 ../build/g-windows-386.exe | cut -d ' ' -f 1) 6 | hasharm64=$(shasum -a 256 ../build/g-windows-arm64.exe | cut -d ' ' -f 1) 7 | 8 | echo "{ \ 9 | \"homepage\": \"g.equationzhao.space\", \ 10 | \"bin\": \"bin/g.exe\", \ 11 | \"architecture\": { \ 12 | \"64bit\": { \ 13 | \"url\": \"https://github.com/Equationzhao/g/releases/download/v$latest/g-windows-amd64.exe\", \ 14 | \"hash\": \"$hash64\", \ 15 | \"bin\": \"g-windows-amd64.exe\", \ 16 | \"post_install\":[ \ 17 | \"cd \$scoopdir/shims\", \ 18 | \"mv g-windows-amd64.exe g.exe\", \ 19 | \"mv g-windows-amd64.shim g.shim\" \ 20 | ], \ 21 | \"shortcuts\":[ \ 22 | [ \ 23 | \"g-windows-amd64.exe\", \ 24 | \"g\" \ 25 | ] \ 26 | ] \ 27 | }, \ 28 | \"32bit\": { \ 29 | \"url\": \"https://github.com/Equationzhao/g/releases/download/v$latest/g-windows-386.exe\", \ 30 | \"hash\": \"$hash386\", \ 31 | \"bin\": \"g-windows-386.exe\", \ 32 | \"post_install\":[ \ 33 | \"cd \$scoopdir/shims\", \ 34 | \"mv g-windows-386.exe g.exe\", \ 35 | \"mv g-windows-386.shim g.shim\" \ 36 | ], \ 37 | \"shortcuts\":[ \ 38 | [ \ 39 | \"g-windows-386.exe\", \ 40 | \"g\" \ 41 | ] \ 42 | ] \ 43 | }, \ 44 | \"arm64\": { \ 45 | \"url\": \"https://github.com/Equationzhao/g/releases/download/v$latest/g-windows-arm64.exe\", \ 46 | \"hash\": \"$hasharm64\", \ 47 | \"bin\": \"g-windows-arm64.exe\", \ 48 | \"post_install\":[ \ 49 | \"cd \$scoopdir/shims\", \ 50 | \"mv g-windows-arm64.exe g.exe\", \ 51 | \"mv g-windows-arm64.shim g.shim\" \ 52 | ], \ 53 | \"shortcuts\":[ \ 54 | [ \ 55 | \"g-windows-arm64.exe\", \ 56 | \"g\" \ 57 | ] \ 58 | ] \ 59 | } \ 60 | }, \ 61 | \"license\": \"MIT\", \ 62 | \"version\": \"v$latest\" \ 63 | }" > g.json 64 | 65 | prettier -w g.json -------------------------------------------------------------------------------- /internal/osbased/filedetail_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package osbased 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "syscall" 9 | "unsafe" 10 | 11 | "github.com/Equationzhao/g/internal/item" 12 | ) 13 | 14 | func Inode(info os.FileInfo) string { 15 | return "-" 16 | } 17 | 18 | var ( 19 | kernel32 = syscall.NewLazyDLL("kernel32.dll") 20 | getFileInformationByHandle = kernel32.NewProc("GetFileInformationByHandle") 21 | ) 22 | 23 | type byHandleFileInformation struct { 24 | FileAttributes uint32 25 | CreationTime syscall.Filetime 26 | LastAccessTime syscall.Filetime 27 | LastWriteTime syscall.Filetime 28 | VolumeSerialNumber uint32 29 | FileSizeHigh uint32 30 | FileSizeLow uint32 31 | NumberOfLinks uint32 32 | FileIndexHigh uint32 33 | FileIndexLow uint32 34 | } 35 | 36 | func getNumberOfHardLinks(info *item.FileInfo) (uint64, error) { 37 | utf16PtrFromString, err := syscall.UTF16PtrFromString(info.FullPath) 38 | if err != nil { 39 | return 0, err 40 | } 41 | handle, err := syscall.CreateFile( 42 | utf16PtrFromString, 43 | 0, 44 | 0, 45 | nil, 46 | syscall.OPEN_EXISTING, 47 | syscall.FILE_FLAG_BACKUP_SEMANTICS, 48 | 0, 49 | ) 50 | if err != nil { 51 | return 0, fmt.Errorf("failed to open file: %v", err) 52 | } 53 | defer func() { 54 | _ = syscall.CloseHandle(handle) 55 | }() 56 | 57 | var fileInfo byHandleFileInformation 58 | ret, _, err := getFileInformationByHandle.Call( 59 | uintptr(handle), 60 | uintptr(unsafe.Pointer(&fileInfo)), 61 | ) 62 | if ret == 0 { 63 | return 0, fmt.Errorf("failed to get file information: %w", err) 64 | } 65 | 66 | return uint64(fileInfo.NumberOfLinks), nil 67 | } 68 | 69 | func LinkCount(info *item.FileInfo) uint64 { 70 | n, err := getNumberOfHardLinks(info) 71 | if err != nil { 72 | return 0 73 | } 74 | return n 75 | } 76 | 77 | func BlockSize(info os.FileInfo) int64 { 78 | return 0 79 | } 80 | 81 | // always false on Windows 82 | func IsMacOSAlias(_ string) bool { 83 | return false 84 | } 85 | 86 | // ResolveAlias is a no-op on Windows. 87 | func ResolveAlias(_ string) (string, error) { 88 | return "", nil 89 | } 90 | -------------------------------------------------------------------------------- /completions/bash/g-completion.bash: -------------------------------------------------------------------------------- 1 | _g() { 2 | local cur prev opts 3 | 4 | COMPREPLY=() 5 | cur="${COMP_WORDS[COMP_CWORD]}" 6 | prev="${COMP_WORDS[COMP_CWORD-1]}" 7 | 8 | opts=" 9 | --bug 10 | --duplicate 11 | --no-config 12 | --no-path-transform 13 | --help -h -? 14 | --version -v -# 15 | --csv 16 | --tsv 17 | --byline -1 18 | --classic 19 | --color 20 | --colorless 21 | --depth 22 | --format 23 | --file-type 24 | --md 25 | --markdown 26 | --table 27 | --table-style 28 | --term-width 29 | --theme 30 | --tree-style 31 | --zero -0 -C -F -R -T -d -j -m -x 32 | --init 33 | --sort 34 | --dir-first 35 | --group-directories-first 36 | --reverse -r 37 | --versionsort -S 38 | --si 39 | --sizesort -U 40 | --no-sort -X 41 | --sort-by-ext 42 | --accessed 43 | --all 44 | --birth 45 | --blocks 46 | --charset 47 | --checksum 48 | --checksum-algorithm 49 | --created 50 | --dereference 51 | --footer 52 | --full-path 53 | --full-time 54 | --flags 55 | --gid 56 | --git 57 | --git-status 58 | --git-repo-branch 59 | --branch 60 | --git-repo-status 61 | --repo-status 62 | --group 63 | --header 64 | --title 65 | --hyperlink 66 | --icon 67 | --inode -i 68 | --mime 69 | --mime-parent 70 | --modified 71 | --mounts 72 | --no-dereference 73 | --no-icon 74 | --no-total-size 75 | --numeric 76 | --numeric-uid-gid 77 | --octal-perm 78 | --owner 79 | --perm 80 | --recursive-size 81 | --relative-to 82 | --relative-time 83 | --size 84 | --size-unit 85 | --block-size 86 | --smart-group 87 | --statistic 88 | --stdin 89 | --time 90 | --time-style 91 | --time-type 92 | --total-size 93 | --uid -G 94 | --no-group -H 95 | --link -N 96 | --literal -O 97 | --no-owner -Q 98 | --quote-name -g -l 99 | --long -o" 100 | 101 | COMPREPLY=($(compgen -W "${opts}" -- ${cur})) 102 | return 0 103 | } 104 | 105 | complete -F _g g 106 | -------------------------------------------------------------------------------- /internal/git/git_status_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "reflect" 5 | "runtime" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestParseShort(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | args string 14 | wantRes FileGits 15 | }{ 16 | { 17 | name: "case 1", 18 | args: "AM internal/git/git_test.go\n!! .DS_Store\n!! .idea/\n!! build/\n", 19 | wantRes: FileGits{ 20 | {Name: "internal/git/git_test.go", X: Added, Y: Modified}, 21 | {Name: ".DS_Store", X: Ignored, Y: Ignored}, 22 | {Name: ".idea", X: Ignored, Y: Ignored}, 23 | {Name: "build", X: Ignored, Y: Ignored}, 24 | }, 25 | }, 26 | { 27 | name: "case 2", 28 | args: "D my_folder/my_file.txt\n", 29 | wantRes: FileGits{ 30 | {Name: "my_folder/my_file.txt", X: Deleted, Y: Unmodified}, 31 | }, 32 | }, 33 | { 34 | name: "case 3", 35 | args: " D my_folder/my_file.txt\n", 36 | wantRes: FileGits{ 37 | {Name: "my_folder/my_file.txt", X: Unmodified, Y: Deleted}, 38 | }, 39 | }, 40 | { 41 | name: "case 4", 42 | args: "T my_folder/my_file.txt\n", 43 | wantRes: FileGits{ 44 | {Name: "my_folder/my_file.txt", X: TypeChanged, Y: Unmodified}, 45 | }, 46 | }, 47 | { 48 | name: "case 5", 49 | args: "M my_folder/file1.txt\nD my_folder/file2.txt\nAU my_folder/file3.txt\n", 50 | wantRes: FileGits{ 51 | {Name: "my_folder/file1.txt", X: Modified, Y: Unmodified}, 52 | {Name: "my_folder/file2.txt", X: Deleted, Y: Unmodified}, 53 | {Name: "my_folder/file3.txt", X: Added, Y: UpdatedButUnmerged}, 54 | }, 55 | }, 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | gotRes := ParseShort(normalizePath(tt.args)) 60 | for i := range gotRes { 61 | gotRes[i].Name = normalizePath(gotRes[i].Name) 62 | } 63 | if !reflect.DeepEqual(gotRes, tt.wantRes) { 64 | t.Errorf("ParseShort() = %v, want %v", gotRes, tt.wantRes) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | func normalizePath(path string) string { 71 | // normalize path according to the OS 72 | switch os := runtime.GOOS; os { 73 | case "windows": 74 | return strings.ReplaceAll(path, "/", "\\") 75 | default: 76 | return strings.ReplaceAll(path, "\\", "/") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/osbased/macos_alias.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | bool isAlias(const char *path) { 10 | if (path == NULL) { 11 | return false; 12 | } 13 | 14 | CFStringRef cfPath = CFStringCreateWithCString(NULL, path, kCFStringEncodingUTF8); 15 | if (cfPath == NULL) { 16 | return false; 17 | } 18 | 19 | CFURLRef url = CFURLCreateWithFileSystemPath(NULL, cfPath, kCFURLPOSIXPathStyle, false); 20 | CFRelease(cfPath); 21 | 22 | if (url == NULL) { 23 | return false; 24 | } 25 | 26 | Boolean isAlias = false; 27 | CFBooleanRef isAliasRef = NULL; 28 | if (CFURLCopyResourcePropertyForKey(url, kCFURLIsAliasFileKey, &isAliasRef, NULL)) { 29 | isAlias = CFBooleanGetValue(isAliasRef); 30 | CFRelease(isAliasRef); 31 | } 32 | CFRelease(url); 33 | 34 | return isAlias; 35 | } 36 | 37 | char *resolveAlias(const char *path) { 38 | if (path == NULL) { 39 | return NULL; 40 | } 41 | 42 | CFURLRef url = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8 *)path, strlen(path), false); 43 | if (!url) { 44 | return NULL; 45 | } 46 | 47 | CFErrorRef error = NULL; 48 | CFDataRef bookmarkData = CFURLCreateBookmarkDataFromFile(NULL, url, &error); 49 | CFRelease(url); 50 | if (!bookmarkData) { 51 | if (error != NULL) { 52 | CFRelease(error); 53 | } 54 | return NULL; 55 | } 56 | 57 | Boolean bookmarkIsStale; 58 | CFURLRef resolvedURL = CFURLCreateByResolvingBookmarkData(NULL, bookmarkData, kCFBookmarkResolutionWithoutUIMask, NULL, NULL, &bookmarkIsStale, &error); 59 | CFRelease(bookmarkData); 60 | if (!resolvedURL) { 61 | if (error != NULL) { 62 | CFRelease(error); 63 | } 64 | return NULL; 65 | } 66 | 67 | UInt8 buffer[PATH_MAX]; 68 | Boolean success = CFURLGetFileSystemRepresentation(resolvedURL, true, buffer, PATH_MAX); 69 | CFRelease(resolvedURL); 70 | if (!success) { 71 | return NULL; 72 | } 73 | 74 | return strdup((const char*)buffer); 75 | } -------------------------------------------------------------------------------- /script/reproduce_test_result.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This script is deprecated" 4 | exit 0 5 | 6 | # Reproduce the test result from the test script 7 | # This script will overwrite the test result in tests/*.stdout 8 | 9 | error() { 10 | printf '\033[1;31m%s\033[0m\n' "$1" 11 | } 12 | 13 | success() { 14 | printf '\033[1;32m%s\033[0m\n' "$1" 15 | } 16 | 17 | warn() { 18 | printf '\033[1;33m%s\033[0m\n' "$1" 19 | } 20 | 21 | bye(){ 22 | echo "Bye👋" 23 | exit 0 24 | } 25 | 26 | check_input(){ 27 | echo 28 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 29 | bye 30 | exit 1 31 | fi 32 | } 33 | 34 | # PRINT WARNING 35 | warn 'This script will overwrite the test result in tests/*.stdout' 36 | 37 | read -p "Are you sure? (y/N) " -n 1 -r 38 | check_input 39 | read -p "Are you sure?? (y/N) " -n 1 -r 40 | check_input 41 | read -p "Are you sure??? (y/N) " -n 1 -r 42 | check_input 43 | 44 | echo "Well, you asked for it..." 45 | 46 | printf 'which one do you want to reproduce?(name/all/none)\n' 47 | 48 | read -p "Enter the name(s) of the test script(s): " -r 49 | if [ "$REPLY" == "all" ]; then 50 | echo "Reproducing all test result..." 51 | for sh_file in tests/*.sh; do 52 | name="${sh_file%.*}" 53 | first_line=$(head -n 2 "$sh_file") 54 | eval "$first_line" 55 | # output is assigned in the test script 56 | echo "$output" > "$name.stdout" 57 | done 58 | success "Test result reproduced successfully.🎉" 59 | exit 0 60 | fi 61 | 62 | if [ "$REPLY" == "none" ]; then 63 | error "No test result will be reproduced." 64 | bye 65 | exit 0 66 | fi 67 | 68 | # split the input by comma 69 | IFS=',' read -r -a test_names <<< "$REPLY" 70 | 71 | # check if the test script exists 72 | for test_name in "${test_names[@]}"; do 73 | sh_file="tests/$test_name.sh" 74 | if [ ! -f "$sh_file" ]; then 75 | error "$sh_file does not exist.😭" 76 | exit 1 77 | fi 78 | done 79 | 80 | # reproduce the test results 81 | for test_name in "${test_names[@]}"; do 82 | echo "Reproducing $sh_file..." 83 | name="${sh_file%.*}" 84 | first_line=$(head -n 2 "$sh_file") 85 | eval "$first_line" 86 | # output is assigned in the test script 87 | echo "$output" > "$name.stdout" 88 | done 89 | 90 | success "Test result reproduced successfully.🎉" -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Equationzhao/g 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/Equationzhao/pathbeautify v0.0.8 7 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d 8 | github.com/agiledragon/gomonkey/v2 v2.11.0 9 | github.com/alphadose/haxmap v1.4.1 10 | github.com/gabriel-vasile/mimetype v1.4.9 11 | github.com/gobwas/glob v0.2.3 12 | github.com/gookit/color v1.5.4 13 | github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b 14 | github.com/jedib0t/go-pretty/v6 v6.6.7 15 | github.com/mattn/go-runewidth v0.0.16 16 | github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 17 | github.com/pkg/xattr v0.4.12 18 | github.com/sahilm/fuzzy v0.1.1 19 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d 20 | github.com/shirou/gopsutil/v3 v3.24.5 21 | github.com/stretchr/testify v1.10.0 22 | github.com/syndtr/goleveldb v1.0.0 23 | github.com/urfave/cli/v2 v2.27.7 24 | github.com/valyala/bytebufferpool v1.0.0 25 | github.com/wk8/go-ordered-map/v2 v2.1.8 26 | github.com/zeebo/assert v1.3.1 27 | github.com/zeebo/xxh3 v1.0.2 28 | golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b 29 | golang.org/x/sys v0.33.0 30 | gopkg.in/yaml.v3 v3.0.1 31 | ) 32 | 33 | require ( 34 | github.com/go-ole/go-ole v1.3.0 // indirect 35 | github.com/klauspost/cpuid/v2 v2.2.11 // indirect 36 | github.com/kylelemons/godebug v1.1.0 // indirect 37 | github.com/onsi/gomega v1.34.1 // indirect 38 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 39 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 40 | ) 41 | 42 | require ( 43 | github.com/bahlo/generic-list-go v0.2.0 // indirect 44 | github.com/buger/jsonparser v1.1.1 // indirect 45 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect 46 | github.com/davecgh/go-spew v1.1.1 // indirect 47 | github.com/golang/snappy v1.0.0 // indirect 48 | github.com/itchyny/timefmt-go v0.1.6 49 | github.com/mailru/easyjson v0.9.0 // indirect 50 | github.com/pmezard/go-difflib v1.0.0 // indirect 51 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 52 | github.com/rivo/uniseg v0.4.7 // indirect 53 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 54 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 55 | golang.org/x/net v0.41.0 // indirect 56 | golang.org/x/text v0.26.0 // indirect 57 | ) 58 | -------------------------------------------------------------------------------- /internal/content/sum.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "crypto/sha1" 7 | "crypto/sha256" 8 | "crypto/sha512" 9 | "fmt" 10 | "hash" 11 | "hash/crc32" 12 | "io" 13 | "os" 14 | 15 | "github.com/Equationzhao/g/internal/align" 16 | "github.com/Equationzhao/g/internal/item" 17 | "github.com/Equationzhao/g/internal/render" 18 | ) 19 | 20 | type SumType string 21 | 22 | const ( 23 | SumTypeMd5 SumType = "MD5" 24 | SumTypeSha1 SumType = "SHA1" 25 | SumTypeSha224 SumType = "SHA224" 26 | SumTypeSha256 SumType = "SHA256" 27 | SumTypeSha384 SumType = "SHA384" 28 | SumTypeSha512 SumType = "SHA512" 29 | SumTypeCRC32 SumType = "CRC32" 30 | ) 31 | 32 | type SumEnabler struct{} 33 | 34 | func (s SumEnabler) EnableSum(renderer *render.Renderer, sumTypes ...SumType) []ContentOption { 35 | options := make([]ContentOption, 0, len(sumTypes)) 36 | factory := func(sumType SumType) ContentOption { 37 | return func(info *item.FileInfo) (string, string) { 38 | if info.IsDir() { 39 | return "", string(sumType) 40 | } 41 | 42 | var content []byte 43 | if content_, ok := info.Cache["content"]; ok { 44 | content = content_ 45 | } else { 46 | file, err := os.Open(info.FullPath) 47 | if err != nil { 48 | return "", string(sumType) 49 | } 50 | content, err = io.ReadAll(file) 51 | if err != nil { 52 | return "", string(sumType) 53 | } 54 | info.Cache["content"] = content 55 | defer file.Close() 56 | } 57 | 58 | var hashed hash.Hash 59 | switch sumType { 60 | case SumTypeMd5: 61 | hashed = md5.New() 62 | case SumTypeSha1: 63 | hashed = sha1.New() 64 | case SumTypeSha224: 65 | hashed = sha256.New224() 66 | case SumTypeSha256: 67 | hashed = sha256.New() 68 | case SumTypeSha384: 69 | hashed = sha512.New384() 70 | case SumTypeSha512: 71 | hashed = sha512.New() 72 | case SumTypeCRC32: 73 | hashed = crc32.NewIEEE() 74 | } 75 | if _, err := io.Copy(hashed, bytes.NewReader(content)); err != nil { 76 | return "", string(sumType) 77 | } 78 | return renderer.Checksum(fmt.Sprintf("%x", hashed.Sum(nil))), string(sumType) 79 | } 80 | } 81 | for _, sumType := range sumTypes { 82 | align.RegisterHeaderFooter(string(sumType)) 83 | options = append(options, factory(sumType)) 84 | } 85 | return options 86 | } 87 | -------------------------------------------------------------------------------- /internal/content/charset.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | constval "github.com/Equationzhao/g/internal/global" 8 | "github.com/Equationzhao/g/internal/item" 9 | "github.com/Equationzhao/g/internal/render" 10 | "github.com/Equationzhao/g/internal/util" 11 | "github.com/gabriel-vasile/mimetype" 12 | "github.com/saintfish/chardet" 13 | ) 14 | 15 | type CharsetEnabler struct{} 16 | 17 | func NewCharsetEnabler() *CharsetEnabler { 18 | return &CharsetEnabler{} 19 | } 20 | 21 | const ( 22 | Charset = constval.NameOfCharset 23 | ) 24 | 25 | func (c *CharsetEnabler) Enable(renderer *render.Renderer) ContentOption { 26 | det := chardet.NewTextDetector() 27 | return func(info *item.FileInfo) (string, string) { 28 | // check cache 29 | if c, ok := info.Cache[Charset]; ok { 30 | return renderer.Charset(string(c)), Charset 31 | } 32 | // only text file has charset 33 | if !isTextFile(info) { 34 | return renderer.Charset("-"), Charset 35 | } 36 | // detect file type 37 | mtype, err := mimetype.DetectFile(info.FullPath) 38 | if err != nil { 39 | return renderer.Charset("failed_to_read"), Charset 40 | } 41 | // detect charset 42 | charset := detectCharset(mtype, info, det) 43 | info.Cache[Charset] = []byte(charset) 44 | return renderer.Charset(charset), Charset 45 | } 46 | } 47 | 48 | func isTextFile(info *item.FileInfo) bool { 49 | return !info.IsDir() && 50 | !util.IsSymLink(info) && 51 | info.Mode()&os.ModeNamedPipe == 0 && 52 | info.Mode()&os.ModeSocket == 0 53 | } 54 | 55 | func detectCharset(mtype *mimetype.MIME, info *item.FileInfo, det *chardet.Detector) string { 56 | if tn := mtype.String(); strings.Contains(tn, ";") { 57 | return strings.SplitN(strings.SplitN(tn, ";", 2)[1], "=", 2)[1] 58 | } 59 | if p := mtype.Parent(); p != nil && strings.Contains(p.String(), "text") { 60 | content, err := readFileContent(info.FullPath) 61 | if err != nil { 62 | return err.Error() 63 | } 64 | best, err := det.DetectBest(content) 65 | if err != nil { 66 | return "failed_to_detect" 67 | } 68 | return best.Charset 69 | } 70 | return "-" 71 | } 72 | 73 | func readFileContent(path string) ([]byte, error) { 74 | file, err := os.Open(path) 75 | if err != nil { 76 | return nil, err 77 | } 78 | defer file.Close() 79 | 80 | content := make([]byte, 1024*1024) 81 | _, err = file.Read(content) 82 | return content, err 83 | } 84 | -------------------------------------------------------------------------------- /internal/util/file_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestMockFileInfo(t *testing.T) { 10 | // Create a new instance of MockFileInfo 11 | modTime := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC) 12 | m := NewMockFileInfo(100, true, "test", os.ModeDir, modTime) 13 | 14 | // Test the Name method 15 | if m.Name() != "test" { 16 | t.Errorf("expected 'test', got %s", m.Name()) 17 | } 18 | 19 | // Test the Size method 20 | if m.Size() != 100 { 21 | t.Errorf("expected 100, got %d", m.Size()) 22 | } 23 | 24 | // Test the Mode method 25 | if m.Mode() != os.ModeDir { 26 | t.Errorf("expected os.ModeDir, got %v", m.Mode()) 27 | } 28 | 29 | // Test the ModTime method 30 | if !m.ModTime().Equal(modTime) { 31 | t.Errorf("expected %s time, got %s time", modTime, m.ModTime()) 32 | } 33 | 34 | // Test the IsDir method 35 | if !m.IsDir() { 36 | t.Errorf("expected true, got false") 37 | } 38 | 39 | // Test the Sys method 40 | if m.Sys() != nil { 41 | t.Errorf("expected nil, got %v", m.Sys()) 42 | } 43 | } 44 | 45 | func TestIsSymLink(t *testing.T) { 46 | // Create a new instance of MockFileInfo 47 | modTime := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC) 48 | m := NewMockFileInfo(100, true, "test", os.ModeSymlink, modTime) 49 | 50 | // Test the IsSymLink method 51 | if !IsSymLink(m) { 52 | t.Errorf("expected true, got false") 53 | } 54 | 55 | m.mode = os.ModeDir 56 | if IsSymLink(m) { 57 | t.Errorf("expected false, got true") 58 | } 59 | } 60 | 61 | func TestIsSymLinkMode(t *testing.T) { 62 | // Test the IsSymLinkMode method 63 | if !IsSymLinkMode(os.ModeSymlink) { 64 | t.Errorf("expected true, got false") 65 | } 66 | 67 | if IsSymLinkMode(os.ModeDir) { 68 | t.Errorf("expected false, got true") 69 | } 70 | } 71 | 72 | func TestIsExecutable(t *testing.T) { 73 | m := NewMockFileInfo(100, true, "test", os.ModePerm, time.Now()) 74 | 75 | // Test the IsExecutable method 76 | if !IsExecutable(m) { 77 | t.Errorf("expected true, got false") 78 | } 79 | 80 | m.mode = os.ModeDir 81 | if IsExecutable(m) { 82 | t.Errorf("expected false, got true") 83 | } 84 | } 85 | 86 | func TestIsExecutableMode(t *testing.T) { 87 | // Test the IsExecutableMode method 88 | if !IsExecutableMode(os.ModePerm) { 89 | t.Errorf("expected true, got false") 90 | } 91 | 92 | if IsExecutableMode(os.ModeDir) { 93 | t.Errorf("expected false, got true") 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /internal/content/mimetype.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/Equationzhao/g/internal/align" 8 | 9 | constval "github.com/Equationzhao/g/internal/global" 10 | "github.com/Equationzhao/g/internal/item" 11 | "github.com/Equationzhao/g/internal/render" 12 | "github.com/Equationzhao/g/internal/util" 13 | "github.com/gabriel-vasile/mimetype" 14 | ) 15 | 16 | type MimeFileTypeEnabler struct { 17 | ParentOnly bool 18 | } 19 | 20 | func NewMimeFileTypeEnabler() *MimeFileTypeEnabler { 21 | return &MimeFileTypeEnabler{ 22 | ParentOnly: false, 23 | } 24 | } 25 | 26 | const ( 27 | MimeTypeName = constval.NameOfMIME 28 | ParentMimeTypeName = "Parent-Mime-type" 29 | ) 30 | 31 | func (e *MimeFileTypeEnabler) Enable(renderer *render.Renderer) ContentOption { 32 | align.RegisterHeaderFooter(MimeTypeName, ParentMimeTypeName) 33 | return func(info *item.FileInfo) (string, string) { 34 | res, returnName := func() (string, string) { 35 | tn := "" 36 | returnName := MimeTypeName 37 | if e.ParentOnly { 38 | returnName = ParentMimeTypeName 39 | } 40 | if c, ok := info.Cache[MimeTypeName]; ok { 41 | tn = string(c) 42 | } else { 43 | if info.IsDir() { 44 | tn = "directory" 45 | return tn, returnName 46 | } 47 | if util.IsSymLink(info) { 48 | tn = "symlink" 49 | return tn, returnName 50 | } 51 | if info.Mode()&os.ModeNamedPipe != 0 { 52 | tn = "named_pipe" 53 | return tn, returnName 54 | } 55 | if info.Mode()&os.ModeSocket != 0 { 56 | tn = "socket" 57 | return tn, returnName 58 | } 59 | if m, ok := info.Cache[MimeTypeName]; ok { 60 | info.Cache[Charset] = m 61 | return string(m), returnName 62 | } 63 | 64 | file, err := os.Open(info.FullPath) 65 | if err != nil { 66 | return "failed_to_read", returnName 67 | } 68 | // nolint 69 | defer file.Close() 70 | mtype, err := mimetype.DetectReader(file) 71 | if err != nil { 72 | return err.Error(), returnName 73 | } 74 | tn = mtype.String() 75 | } 76 | 77 | if e.ParentOnly { 78 | tn = strings.SplitN(tn, "/", 2)[0] 79 | } 80 | 81 | if strings.Contains(tn, ";") { 82 | // remove charset 83 | s := strings.SplitN(tn, ";", 2) 84 | tn = s[0] 85 | charset := strings.SplitN(s[1], "=", 2)[1] 86 | info.Cache[Charset] = []byte(charset) 87 | } 88 | 89 | return tn, returnName 90 | }() 91 | return renderer.Mime(res), returnName 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /internal/display/tree/tree.go: -------------------------------------------------------------------------------- 1 | package tree 2 | 3 | import "github.com/Equationzhao/g/internal/item" 4 | 5 | /* 6 | build tree like this: 7 | drwxr-xr-x@ - mr.black 10 7 03:38 ├── filter 8 | drwxr-xr-x@ - mr.black 10 7 03:38 │ ├── content 9 | .rw-r--r--@ 1.7k mr.black 10 7 03:38 │ │ ├── charset.go 10 | .rw-r--r--@ 4.3k mr.black 7 7 23:39 │ │ ├── duplicate.go 11 | .rw-r--r--@ 2.5k mr.black 7 7 20:35 │ │ ├── git.go 12 | .rw-r--r--@ 949 mr.black 5 7 01:26 │ │ ├── group.go 13 | .rw-r--r--@ 323 mr.black 5 7 01:26 │ │ ├── index.go 14 | .rw-r--r--@ 479 mr.black 10 7 03:38 │ │ ├── inode.go 15 | .rw-r--r--@ 564 mr.black 5 7 01:26 │ │ ├── link.go 16 | .rw-r--r--@ 1.5k mr.black 10 7 03:38 │ │ ├── mimetype.go 17 | .rw-r--r--@ 5.8k mr.black 7 7 20:35 │ │ ├── name.go 18 | .rw-r--r--@ 972 mr.black 5 7 01:26 │ │ ├── owner.go 19 | .rw-r--r--@ 743 mr.black 5 7 01:26 │ │ ├── permission.go 20 | .rw-r--r--@ 5.5k mr.black 10 7 03:38 │ │ ├── size.go 21 | .rw-r--r--@ 2.7k mr.black 7 7 20:35 │ │ ├── sum.go 22 | .rw-r--r--@ 1.7k mr.black 7 7 20:35 │ │ └── time.go 23 | .rw-r--r--@ 2.8k mr.black 10 7 03:38 │ ├── contentfilter.go 24 | .rw-r--r--@ 5.0k mr.black 10 7 03:38 │ └── itemfliter.go 25 | .rw-r--r--@ 9.6k mr.black 7 7 23:39 ├── g.md 26 | ... 27 | */ 28 | 29 | type Node struct { 30 | Parent *Node 31 | Child []*Node 32 | Connectors []string 33 | Level int 34 | Meta *item.FileInfo 35 | } 36 | 37 | func (n *Node) Apply2Child(f func(node *Node)) { 38 | if n.Child == nil { 39 | return 40 | } 41 | for _, child := range n.Child { 42 | f(child) 43 | child.Apply2Child(f) 44 | } 45 | } 46 | 47 | func (n *Node) AddChild(child *Node) *Node { 48 | n.Child = append(n.Child, child) 49 | child.Parent = n 50 | child.Level = n.Level + 1 51 | return n 52 | } 53 | 54 | func (n *Node) Apply2ChildSlice(connectors func(nodes []*Node)) { 55 | if n.Child == nil { 56 | return 57 | } 58 | connectors(n.Child) 59 | for _, child := range n.Child { 60 | child.Apply2ChildSlice(connectors) 61 | } 62 | } 63 | 64 | func (n *Node) ApplyThis(p func(node *Node)) { 65 | p(n) 66 | } 67 | 68 | type Tree struct { 69 | Root *Node 70 | } 71 | 72 | type Option = func(tree *Tree) 73 | 74 | func WithCap(cap int) Option { 75 | return func(tree *Tree) { 76 | tree.Root.Child = make([]*Node, 0, cap) 77 | } 78 | } 79 | 80 | func NewTree(ops ...Option) *Tree { 81 | t := &Tree{ 82 | Root: &Node{ 83 | Parent: nil, 84 | Level: 0, 85 | Connectors: nil, 86 | }, 87 | } 88 | for _, op := range ops { 89 | op(t) 90 | } 91 | if t.Root.Child == nil { 92 | t.Root.Child = make([]*Node, 0, 10) 93 | } 94 | return t 95 | } 96 | -------------------------------------------------------------------------------- /internal/display/header.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/Equationzhao/g/internal/align" 8 | constval "github.com/Equationzhao/g/internal/global" 9 | "github.com/Equationzhao/g/internal/item" 10 | "github.com/valyala/bytebufferpool" 11 | ) 12 | 13 | type HeaderMaker struct { 14 | Header, Footer bool 15 | IsBefore bool 16 | LongestEachPart map[string]int 17 | AllPart []string 18 | } 19 | 20 | func (h HeaderMaker) Make(p Printer, Items ...*item.FileInfo) { 21 | // add header 22 | if len(Items) == 0 { 23 | return 24 | } 25 | 26 | // add longest - len(header) * space 27 | // print header 28 | headerFooterStrBuf := bytebufferpool.Get() 29 | defer bytebufferpool.Put(headerFooterStrBuf) 30 | prettyPrinter, isPrettyPrinter := p.(PrettyPrinter) 31 | 32 | expand := func(s string, no, space int) { 33 | // left align 34 | if no != len(h.AllPart)-1 && align.IsLeftHeaderFooter(s) { 35 | _, _ = headerFooterStrBuf.WriteString(strings.Repeat(" ", space-1)) // remove the additional following space for right align 36 | } 37 | _, _ = headerFooterStrBuf.WriteString(constval.Underline) 38 | _, _ = headerFooterStrBuf.WriteString(s) 39 | _, _ = headerFooterStrBuf.WriteString(constval.Reset) 40 | if no != len(h.AllPart)-1 { 41 | if !align.IsLeftHeaderFooter(s) { 42 | _, _ = headerFooterStrBuf.WriteString(strings.Repeat(" ", space)) 43 | } else { 44 | _, _ = headerFooterStrBuf.WriteString(strings.Repeat(" ", 1)) // still need the following space for left align 45 | } 46 | } 47 | } 48 | 49 | for i, s := range h.AllPart { 50 | if len(s) > h.LongestEachPart[s] { 51 | // expand the every item's content of this part 52 | for _, it := range Items { 53 | content, _ := it.Get(s) 54 | if s != constval.NameOfName { 55 | toAddNum := len(s) - WidthNoHyperLinkLen(content.String()) 56 | if align.IsLeft(s) { 57 | content.AddSuffix(strings.Repeat(" ", toAddNum)) 58 | } else { 59 | content.AddPrefix(strings.Repeat(" ", toAddNum)) 60 | } 61 | } 62 | it.Set(s, content) 63 | h.LongestEachPart[s] = len(s) 64 | } 65 | expand(s, i, 1) 66 | } else { 67 | expand(s, i, h.LongestEachPart[s]-len(s)+1) 68 | } 69 | if isPrettyPrinter && h.IsBefore { 70 | if h.Header { 71 | prettyPrinter.AddHeader(s) 72 | } 73 | if h.Footer { 74 | prettyPrinter.AddFooter(s) 75 | } 76 | } 77 | } 78 | res := headerFooterStrBuf.String() 79 | if !isPrettyPrinter { 80 | if h.Header && h.IsBefore { 81 | _, _ = fmt.Fprintln(p, res) 82 | } 83 | if h.Footer && !h.IsBefore { 84 | _, _ = fmt.Fprintln(p, res) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/global/const.go: -------------------------------------------------------------------------------- 1 | // Package global contains the global variables used in the project 2 | // this package can't depend on other packages 3 | package global 4 | 5 | import ( 6 | "golang.org/x/exp/constraints" 7 | ) 8 | 9 | // Hashable is the type of values that may be used as map keys or set members. 10 | // it should be exact same as haxmap.hashable (it's unexported, so we can't use it directly) 11 | type Hashable interface { 12 | constraints.Integer | constraints.Float | constraints.Complex | ~string | uintptr 13 | } 14 | 15 | const ( 16 | Black = "\033[0;30m" // 0,0,0 17 | Red = "\033[0;31m" // 205,0,0 18 | Green = "\033[0;32m" // 0,205,0 19 | Yellow = "\033[0;33m" // 205,205,0 20 | Blue = "\033[0;34m" // 0,0,238 21 | Purple = "\033[0;35m" // 205,0,205 22 | Cyan = "\033[0;36m" // 0,205,205 23 | White = "\033[0;37m" // 229,229,229 24 | BrightBlack = "\033[0;90m" // 127,127,127 25 | BrightRed = "\033[0;91m" // 255,0,0 26 | BrightGreen = "\033[0;92m" // 0,255,0 27 | BrightYellow = "\033[0;93m" // 255,255,0 28 | BrightBlue = "\033[0;94m" // 92,92,255 29 | BrightPurple = "\033[0;95m" // 255,0,255 30 | BrightCyan = "\033[0;96m" // 0,255,255 31 | BrightWhite = "\033[0;97m" // 255,255,255 32 | Success = Green 33 | Error = Red 34 | Warn = Yellow 35 | Bold = "\033[1m" 36 | Faint = "\033[2m" 37 | Italics = "\033[3m" 38 | Underline = "\033[4m" 39 | Blink = "\033[5m" 40 | Reverse = "\033[7m" 41 | ReverseDone = "\033[27m" 42 | ) 43 | 44 | const ( 45 | BoostThreshold = 0.7 46 | PrefixSize = 4 47 | ) 48 | 49 | const Reset = "\033[0m" 50 | 51 | const ( 52 | DefaultHookLen = 5 53 | Space = 2 54 | ) 55 | 56 | const ( 57 | NameOfName = "Name" 58 | NameOfCharset = "Charset" 59 | NameOfMIME = "Mime-type" 60 | NameOfLink = "Link" 61 | NameOfInode = "Inode" 62 | NameOfIndex = "#" 63 | NameOfGroupName = "Group" 64 | NameOfGroupUidName = "Group-uid" 65 | NameOfGroupSID = "Group-sid" 66 | NameOfOwner = "Owner" 67 | NameOfOwnerUid = "Owner-uid" 68 | NameOfOwnerSID = "Owner-sid" 69 | NameOfSize = "Size" 70 | NameOfGitStatus = "Git" 71 | NameOfGitRepoBranch = "Branch" 72 | NameOfGitRepoStatus = "Repo-status" 73 | NameOfGitCommitHash = "Commit-Hash" 74 | NameOfGitAuthor = "Git-Author" 75 | NameOfGitAuthorDate = "Author-Date" 76 | NameOfPermission = "Permissions" 77 | NameOfSum = "Sum" 78 | NameOfRelativeTime = "Relative-Time" 79 | NameOfTime = "Time" 80 | NameOfTimeModified = "Modified" 81 | NameOfTimeCreated = "Created" 82 | NameOfTimeAccessed = "Accessed" 83 | NameOfTimeBirth = "Birth" 84 | NameOfFlags = "Flags" 85 | ) 86 | -------------------------------------------------------------------------------- /internal/config/load.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/Equationzhao/g/internal/util" 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | const ( 14 | NoConfig = "-no-config" 15 | DefaultConfigFile = "g.yaml" 16 | ) 17 | 18 | func GetUserConfigDir() (string, error) { 19 | err := InitConfigDir.Do( 20 | func() error { 21 | home, err := os.UserConfigDir() 22 | if err != nil { 23 | return err 24 | } 25 | Dir = filepath.Join(home, "g") 26 | err = os.MkdirAll(Dir, os.ModePerm) 27 | if err != nil { 28 | return err 29 | } 30 | return nil 31 | }, 32 | ) 33 | if err != nil { 34 | return "", err 35 | } 36 | return Dir, nil 37 | } 38 | 39 | var ( 40 | InitConfigDir util.Once 41 | Dir = "" 42 | ) 43 | 44 | // READ config 45 | // g.yaml 46 | // Args: 47 | // - args 48 | // - ... 49 | 50 | type Config struct { 51 | Args []string `yaml:"Args"` 52 | CustomTreeStyle TreeStyle `yaml:"CustomTreeStyle"` 53 | ThemeLocation string `yaml:"Theme"` 54 | } 55 | 56 | type TreeStyle struct { 57 | Child string `yaml:"Child"` 58 | LastChild string `yaml:"LastChild"` 59 | Mid string `yaml:"Mid"` 60 | Empty string `yaml:"Empty"` 61 | } 62 | 63 | func (t TreeStyle) IsEmpty() bool { 64 | return t.Empty == "" && t.Child == "" && t.LastChild == "" && t.Mid == "" 65 | } 66 | 67 | func (t TreeStyle) IsEnabled() bool { 68 | return !t.IsEmpty() 69 | } 70 | 71 | type ErrReadConfig struct { 72 | error 73 | Location string 74 | } 75 | 76 | func (e ErrReadConfig) Error() string { 77 | if e.Location != "" { 78 | return fmt.Sprintf("failed to load configuration at %s: %s", e.Location, e.error.Error()) 79 | } 80 | return fmt.Sprintf("failed to load configuration: %s", e.error.Error()) 81 | } 82 | 83 | var Default = Config{ 84 | Args: make([]string, 0), 85 | } 86 | 87 | var emptyConfig = Config{} 88 | 89 | func Load() (*Config, error) { 90 | Dir, err := GetUserConfigDir() 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | location := filepath.Join(Dir, DefaultConfigFile) 96 | content, err := os.ReadFile(location) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | // parse yaml 102 | configErr := yaml.Unmarshal(content, &Default) 103 | if configErr != nil { 104 | return nil, ErrReadConfig{error: configErr, Location: location} 105 | } 106 | 107 | for i, v := range Default.Args { 108 | if v == NoConfig { 109 | Default = emptyConfig 110 | return nil, nil 111 | } 112 | // if not prefixed with '-', add '-' 113 | if !strings.HasPrefix(v, "-") { 114 | if len(v) == 1 { 115 | Default.Args[i] = "-" + v 116 | } else { 117 | Default.Args[i] = "--" + v 118 | } 119 | } 120 | } 121 | 122 | return &Default, nil 123 | } 124 | -------------------------------------------------------------------------------- /internal/content/time.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "runtime" 5 | "strings" 6 | "time" 7 | 8 | constval "github.com/Equationzhao/g/internal/global" 9 | "github.com/Equationzhao/g/internal/item" 10 | "github.com/Equationzhao/g/internal/osbased" 11 | "github.com/Equationzhao/g/internal/render" 12 | strftime "github.com/itchyny/timefmt-go" 13 | ) 14 | 15 | type RelativeTimeEnabler struct { 16 | Mode string 17 | } 18 | 19 | func NewRelativeTimeEnabler() *RelativeTimeEnabler { 20 | return &RelativeTimeEnabler{} 21 | } 22 | 23 | const RelativeTime = constval.NameOfRelativeTime 24 | 25 | func (r *RelativeTimeEnabler) Enable(renderer *render.Renderer) ContentOption { 26 | return func(info *item.FileInfo) (string, string) { 27 | var t time.Time 28 | timeType := "" 29 | switch r.Mode { 30 | case "mod": 31 | t = osbased.ModTime(info) 32 | timeType = timeModified 33 | case "create": 34 | t = osbased.CreateTime(info) 35 | timeType = timeCreated 36 | case "access": 37 | t = osbased.AccessTime(info) 38 | timeType = timeAccessed 39 | case "birth": 40 | timeType = timeBirth 41 | // if darwin, check birth time 42 | if runtime.GOOS == "darwin" { 43 | t = osbased.BirthTime(info) 44 | } else { 45 | t = osbased.CreateTime(info) 46 | } 47 | default: 48 | t = osbased.ModTime(info) 49 | timeType = timeModified 50 | } 51 | return renderer.RTime(time.Now(), t), RelativeTime + " " + timeType 52 | } 53 | } 54 | 55 | const ( 56 | timeName = constval.NameOfTime 57 | timeModified = constval.NameOfTimeModified 58 | timeCreated = constval.NameOfTimeCreated 59 | timeAccessed = constval.NameOfTimeAccessed 60 | timeBirth = constval.NameOfTimeBirth 61 | ) 62 | 63 | // EnableTime enables time 64 | // accepts ['mod', 'modified', 'create', 'access', 'birth'] 65 | func EnableTime(format, mode string, renderer *render.Renderer) ContentOption { 66 | return func(info *item.FileInfo) (string, string) { 67 | // get mod time/ create time/ access time 68 | var t time.Time 69 | timeType := "" 70 | switch mode { 71 | case "mod", "modified": 72 | t = osbased.ModTime(info) 73 | timeType = timeModified 74 | case "create", "cr": 75 | t = osbased.CreateTime(info) 76 | timeType = timeCreated 77 | case "access", "ac": 78 | t = osbased.AccessTime(info) 79 | timeType = timeAccessed 80 | case "birth": 81 | timeType = timeBirth 82 | // if darwin, check birth time 83 | if runtime.GOOS == "darwin" { 84 | t = osbased.BirthTime(info) 85 | } else { 86 | t = osbased.CreateTime(info) 87 | } 88 | default: 89 | t = osbased.ModTime(info) 90 | timeType = timeModified 91 | } 92 | 93 | var timeString string 94 | if strings.HasPrefix(format, "+") { 95 | timeString = strftime.Format(t, strings.TrimPrefix(format, "+")) 96 | } else { 97 | timeString = t.Format(format) 98 | } 99 | return renderer.Time(timeString), timeName + " " + timeType 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /internal/util/file.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/Equationzhao/g/internal/item" 11 | "github.com/Equationzhao/g/internal/osbased" 12 | ) 13 | 14 | func Evallinks(fullPath string) (string, error) { 15 | // support symlinks and macOS Alias 16 | if osbased.IsMacOSAlias(fullPath) { 17 | for { 18 | if osbased.IsMacOSAlias(fullPath) { 19 | aliasTarget, err := osbased.ResolveAlias(fullPath) 20 | if err != nil { 21 | return "", fmt.Errorf("alias resolution failed: %w", err) 22 | } 23 | fullPath = aliasTarget 24 | } else { 25 | break 26 | } 27 | } 28 | } 29 | return filepath.EvalSymlinks(fullPath) 30 | } 31 | 32 | func IsSymLink(file os.FileInfo) bool { 33 | return file.Mode()&os.ModeSymlink != 0 34 | } 35 | 36 | func IsSymLinkMode(mode os.FileMode) bool { 37 | return mode&os.ModeSymlink != 0 38 | } 39 | 40 | func IsExecutable(file os.FileInfo) bool { 41 | return file.Mode()&0o111 != 0 42 | } 43 | 44 | func IsExecutableMode(mode os.FileMode) bool { 45 | return mode&0o111 != 0 46 | } 47 | 48 | // RecursivelySizeOf returns the size of the file or directory 49 | // depth < 0 means no limit 50 | func RecursivelySizeOf(info *item.FileInfo, depth int) int64 { 51 | currentDepth := 0 52 | if info.IsDir() { 53 | totalSize := int64(0) 54 | if depth < 0 { 55 | // -1 means no limit 56 | _ = filepath.Walk(info.FullPath, func(path string, dir fs.FileInfo, err error) error { 57 | if err != nil { 58 | return err 59 | } 60 | totalSize += dir.Size() 61 | return nil 62 | }) 63 | } else { 64 | _ = filepath.Walk(info.FullPath, func(path string, dir fs.FileInfo, err error) error { 65 | if err != nil { 66 | return err 67 | } 68 | if currentDepth > depth { 69 | if dir.IsDir() { 70 | return filepath.SkipDir 71 | } 72 | return nil 73 | } 74 | totalSize += dir.Size() 75 | if dir.IsDir() { 76 | currentDepth++ 77 | } 78 | return nil 79 | }) 80 | } 81 | return totalSize 82 | } 83 | return info.Size() 84 | } 85 | 86 | type MockFileInfo struct { 87 | size int64 88 | isDir bool 89 | name string 90 | mode os.FileMode 91 | modTime time.Time 92 | } 93 | 94 | func NewMockFileInfo(size int64, isDir bool, name string, mode os.FileMode, modTime time.Time) *MockFileInfo { 95 | return &MockFileInfo{size: size, isDir: isDir, name: name, mode: mode, modTime: modTime} 96 | } 97 | 98 | func (m *MockFileInfo) Size() int64 { 99 | return m.size 100 | } 101 | 102 | func (m *MockFileInfo) IsDir() bool { 103 | return m.isDir 104 | } 105 | 106 | func (m *MockFileInfo) Mode() os.FileMode { 107 | return m.mode 108 | } 109 | 110 | func (m *MockFileInfo) ModTime() time.Time { 111 | return m.modTime 112 | } 113 | 114 | func (m *MockFileInfo) Name() string { 115 | return m.name 116 | } 117 | 118 | func (m *MockFileInfo) Sys() any { 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /internal/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | func TestSplitNumberAndUnit(t *testing.T) { 6 | type args struct { 7 | s string 8 | } 9 | tests := []struct { 10 | name string 11 | args args 12 | want float64 13 | want1 string 14 | }{ 15 | { 16 | name: "123bit", 17 | args: args{ 18 | s: "123bit", 19 | }, 20 | want: 123, 21 | want1: "bit", 22 | }, 23 | { 24 | name: "123", 25 | args: args{ 26 | s: "123", 27 | }, 28 | want: 123, 29 | want1: "", 30 | }, 31 | { 32 | name: "1,234.321bit", 33 | args: args{ 34 | s: "1,234.321bit", 35 | }, 36 | want: 1234.321, 37 | want1: "bit", 38 | }, 39 | { 40 | name: "-1,234.321bit", 41 | args: args{ 42 | s: "-1,234.321bit", 43 | }, 44 | want: -1234.321, 45 | want1: "bit", 46 | }, 47 | { 48 | name: "bit", 49 | args: args{ 50 | s: "bit", 51 | }, 52 | want: 0, 53 | want1: "bit", 54 | }, 55 | } 56 | for _, tt := range tests { 57 | t.Run(tt.name, func(t *testing.T) { 58 | got, got1 := SplitNumberAndUnit(tt.args.s) 59 | if got != tt.want { 60 | t.Errorf("SplitNumberAndUnit() got = %v, want %v", got, tt.want) 61 | } 62 | if got1 != tt.want1 { 63 | t.Errorf("SplitNumberAndUnit() got1 = %v, want %v", got1, tt.want1) 64 | } 65 | }) 66 | } 67 | } 68 | 69 | func TestMakeLink(t *testing.T) { 70 | link := MakeLink("abs", "name") 71 | if link != "\033]8;;abs\033\\name\033]8;;\033\\" { 72 | t.Errorf("MakeLink failed") 73 | } 74 | } 75 | 76 | func TestRemoveSep(t *testing.T) { 77 | sep := RemoveSep("a/b/c") 78 | if sep != "a/b/c" { 79 | t.Errorf("RemoveSep failed") 80 | } 81 | sep = RemoveSep("a/b/c/") 82 | if sep != "a/b/c" { 83 | t.Errorf("RemoveSep failed") 84 | } 85 | } 86 | 87 | func TestEscape(t *testing.T) { 88 | type args struct { 89 | a string 90 | } 91 | tests := []struct { 92 | name string 93 | args args 94 | want string 95 | }{ 96 | { 97 | name: "tab", 98 | args: args{ 99 | a: "\t", 100 | }, 101 | want: "\x1b[7m\\t\x1b[27m", 102 | }, 103 | { 104 | name: "carriage return", 105 | args: args{ 106 | a: "\r", 107 | }, 108 | want: "\x1b[7m\\r\x1b[27m", 109 | }, 110 | { 111 | name: "line feed", 112 | args: args{ 113 | a: "\n", 114 | }, 115 | want: "\x1b[7m\\n\x1b[27m", 116 | }, 117 | { 118 | name: "double quote", 119 | args: args{ 120 | a: "\"", 121 | }, 122 | want: "\x1b[7m\\\"\x1b[27m", 123 | }, 124 | { 125 | name: "backslash", 126 | args: args{ 127 | a: "\\", 128 | }, 129 | want: "\x1b[7m\\\\\x1b[27m", 130 | }, 131 | { 132 | name: "single quote", 133 | args: args{ 134 | a: "'", 135 | }, 136 | want: "'", 137 | }, 138 | { 139 | name: "normal", 140 | args: args{ 141 | a: "normal", 142 | }, 143 | want: "normal", 144 | }, 145 | } 146 | for _, tt := range tests { 147 | t.Run(tt.name, func(t *testing.T) { 148 | if got := Escape(tt.args.a); got != tt.want { 149 | t.Errorf("Escape() = %v, want %v", got, tt.want) 150 | } 151 | }) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /internal/theme/default_test.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import "testing" 4 | 5 | func TestAll_UnmarshalJSON(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | b []byte 9 | wantErr bool 10 | wantErrMsg string 11 | }{ 12 | { 13 | name: "nil", 14 | b: nil, 15 | wantErr: true, 16 | wantErrMsg: "unexpected end of JSON input", 17 | }, 18 | { 19 | name: "all", 20 | b: []byte(`{"info": {"-": {"color": "white","bold": true,"icon": "","faint": true,"italics": true,"blink": true}},"permission": {"-": {"color": "BrightBlack"}},"size": {"-": {"color": "white"}},"user": {"owner": {"color": "yellow","bold": true}},"group": {"group": {"color": "yellow","bold": true}},"symlink": {"link-num": {"color": "red"}},"git": {"git-branch": {"color": "yellow"}},"name": {".azure": {"color": "white","icon": ""}},"special": {"char": {"color": "yellow","icon": ""}},"ext": {".profile": {"color": "BrightPreen","icon": ""}}}`), 21 | wantErr: false, 22 | }, 23 | { 24 | name: "failed key", 25 | b: []byte(`{"info": {"-": {"color": "white","failed_key": true,"icon": "","faint": true,"italics": true,"blink": true}},"permission": {"-": {"color": "BrightBlack"}},"size": {"-": {"color": "white"}},"user": {"owner": {"color": "yellow","bold": true}},"group": {"group": {"color": "yellow","bold": true}},"symlink": {"link-num": {"color": "red"}},"git": {"git-branch": {"color": "yellow"}},"name": {".azure": {"color": "white","icon": ""}},"special": {"char": {"color": "yellow","icon": ""}},"ext": {".profile": {"color": "BrightPreen","icon": ""}}}`), 26 | wantErr: true, 27 | wantErrMsg: "failed at key 'info': failed at key '-': unknown field: 'failed_key'", 28 | }, 29 | { 30 | name: "unknown field", 31 | b: []byte(`{"unknown_field": {"-": {"color": "white","bold": true,"icon": "","faint": true,"italics": true,"blink": true}},"permission": {"-": {"color": "BrightBlack"}},"size": {"-": {"color": "white"}},"user": {"owner": {"color": "yellow","bold": true}},"group": {"group": {"color": "yellow","bold": true}},"symlink": {"link-num": {"color": "red"}},"git": {"git-branch": {"color": "yellow"}},"name": {".azure": {"color": "white","icon": ""}},"special": {"char": {"color": "yellow","icon": ""}},"ext": {".profile": {"color": "BrightPreen","icon": ""}}}`), 32 | wantErr: true, 33 | wantErrMsg: "unknown field: 'unknown_field'", 34 | }, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | a := All{} 39 | err := a.UnmarshalJSON(tt.b) 40 | if (err != nil) != tt.wantErr { 41 | t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) 42 | } 43 | if err != nil { 44 | if err.Error() != tt.wantErrMsg { 45 | t.Errorf("UnmarshalJSON() error = %v, wantErrMsg %v", err.Error(), tt.wantErrMsg) 46 | } 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func TestAll_CheckLowerCase(t *testing.T) { 53 | DefaultAll.CheckLowerCase() 54 | } 55 | 56 | func TestAll_CheckLowerCase1(t *testing.T) { 57 | defer func() { 58 | if r := recover(); r == nil { 59 | t.Errorf("The code should panic") 60 | } else { 61 | t.Logf("Recovered from panic: %v", r) 62 | } 63 | }() 64 | 65 | a := All{ 66 | InfoTheme: Theme{"Info": {}}, 67 | } 68 | a.CheckLowerCase() 69 | } 70 | -------------------------------------------------------------------------------- /internal/content/contentfilter.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "runtime" 5 | "slices" 6 | "sync" 7 | 8 | "github.com/Equationzhao/g/internal/display" 9 | "github.com/Equationzhao/g/internal/item" 10 | ) 11 | 12 | type ContentFilter struct { 13 | noOutputOptions []NoOutputOption 14 | options []ContentOption 15 | sortFunc func(a, b *item.FileInfo) int 16 | LimitN uint // <=0 means no limit 17 | } 18 | 19 | func (cf *ContentFilter) SortFunc() func(a, b *item.FileInfo) int { 20 | return cf.sortFunc 21 | } 22 | 23 | func (cf *ContentFilter) SetSortFunc(sortFunc func(a, b *item.FileInfo) int) { 24 | cf.sortFunc = sortFunc 25 | } 26 | 27 | func (cf *ContentFilter) AppendToOptions(options ...ContentOption) { 28 | cf.options = append(cf.options, options...) 29 | } 30 | 31 | func (cf *ContentFilter) SetOptions(options ...ContentOption) { 32 | cf.options = options 33 | } 34 | 35 | func (cf *ContentFilter) AppendToNoOutputOptions(options ...NoOutputOption) { 36 | cf.noOutputOptions = append(cf.noOutputOptions, options...) 37 | } 38 | 39 | func (cf *ContentFilter) SetNoOutputOptions(outputFunc ...NoOutputOption) { 40 | cf.noOutputOptions = outputFunc 41 | } 42 | 43 | type ( 44 | ContentOption func(info *item.FileInfo) (stringContent, funcName string) 45 | NoOutputOption func(info *item.FileInfo) 46 | ) 47 | 48 | type ContentFilterOption func(cf *ContentFilter) 49 | 50 | func WithOptions(options ...ContentOption) ContentFilterOption { 51 | return func(cf *ContentFilter) { 52 | cf.options = options 53 | } 54 | } 55 | 56 | func WithNoOutputOptions(options ...NoOutputOption) ContentFilterOption { 57 | return func(cf *ContentFilter) { 58 | cf.noOutputOptions = options 59 | } 60 | } 61 | 62 | func NewContentFilter(options ...ContentFilterOption) *ContentFilter { 63 | c := &ContentFilter{ 64 | sortFunc: nil, 65 | } 66 | 67 | for _, option := range options { 68 | option(c) 69 | } 70 | 71 | if c.options == nil { 72 | c.options = make([]ContentOption, 0) 73 | } 74 | if c.noOutputOptions == nil { 75 | c.noOutputOptions = make([]NoOutputOption, 0) 76 | } 77 | 78 | return c 79 | } 80 | 81 | func (cf *ContentFilter) GetDisplayItems(e *[]*item.FileInfo) { 82 | if cf.sortFunc != nil { 83 | slices.SortFunc(*e, cf.sortFunc) 84 | } 85 | 86 | // limit number of entries 87 | // 0 means no limit 88 | if cf.LimitN > 0 && len(*e) > int(cf.LimitN) { 89 | *e = (*e)[:cf.LimitN] 90 | } 91 | 92 | maxGoroutines := 10 * runtime.NumCPU() 93 | sem := make(chan struct{}, maxGoroutines) 94 | wg := sync.WaitGroup{} 95 | wg.Add(len(*e)) 96 | 97 | for _, entry := range *e { 98 | entry := entry 99 | sem <- struct{}{} 100 | go func(e *item.FileInfo) { 101 | defer wg.Done() 102 | defer func() { <-sem }() 103 | _ = cf.processEntry(e) 104 | }(entry) 105 | } 106 | wg.Wait() 107 | } 108 | 109 | func (cf *ContentFilter) processEntry(entry *item.FileInfo) error { 110 | for j, option := range cf.options { 111 | stringContent, funcName := option(entry) 112 | content := display.ItemContent{Content: display.StringContent(stringContent), No: j} 113 | entry.Set(funcName, &content) 114 | } 115 | 116 | for _, option := range cf.noOutputOptions { 117 | option(entry) 118 | } 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /internal/index/pathindex.go: -------------------------------------------------------------------------------- 1 | //go:build fuzzy 2 | 3 | package index 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/Equationzhao/g/internal/config" 12 | gutil "github.com/Equationzhao/g/internal/util" 13 | "github.com/sahilm/fuzzy" 14 | "github.com/syndtr/goleveldb/leveldb" 15 | ) 16 | 17 | var ( 18 | db *leveldb.DB 19 | initOnce gutil.Once 20 | closeOnce gutil.Once 21 | indexPath string 22 | ) 23 | 24 | func getDB() (*leveldb.DB, error) { 25 | err := initOnce.Do(func() error { 26 | var err error 27 | indexPath, err = config.GetUserConfigDir() 28 | if err != nil { 29 | return err 30 | } 31 | indexPath = filepath.Join(indexPath, "index") 32 | err = os.MkdirAll(indexPath, os.ModePerm) 33 | if err != nil { 34 | return err 35 | } 36 | db, err = leveldb.OpenFile(indexPath, nil) 37 | if err != nil { 38 | return err 39 | } 40 | return nil 41 | }) 42 | return db, err 43 | } 44 | 45 | func Close() error { 46 | return closeDB() 47 | } 48 | 49 | func closeDB() error { 50 | err := closeOnce.Do(func() error { 51 | if db != nil { 52 | err := db.Close() 53 | if err != nil { 54 | return err 55 | } 56 | } 57 | return nil 58 | }) 59 | return err 60 | } 61 | 62 | type ErrUpdate struct { 63 | key string 64 | } 65 | 66 | func (e ErrUpdate) Error() string { 67 | return fmt.Sprint("failed to update `", e.key, "`") 68 | } 69 | 70 | func Update(key string) error { 71 | db, err := getDB() 72 | if err != nil { 73 | return err 74 | } 75 | _, err = db.Get([]byte(key), nil) 76 | if err != nil { 77 | err := db.Put([]byte(key), []byte("1"), nil) 78 | if err != nil { 79 | return err 80 | } 81 | } else { 82 | return nil 83 | } 84 | return nil 85 | } 86 | 87 | func Delete(key string) error { 88 | db, err := getDB() 89 | if err != nil { 90 | return err 91 | } 92 | return db.Delete([]byte(key), nil) 93 | } 94 | 95 | func RebuildIndex() error { 96 | db, err := getDB() 97 | if err != nil { 98 | goto remove 99 | } 100 | err = db.Close() 101 | if err != nil { 102 | return err 103 | } 104 | remove: 105 | err = os.RemoveAll(indexPath) 106 | if err != nil { 107 | return err 108 | } 109 | return nil 110 | } 111 | 112 | func All() ([]string, error) { 113 | db, err := getDB() 114 | if err != nil { 115 | return nil, err 116 | } 117 | keys := make([]string, 0) 118 | iter := db.NewIterator(nil, nil) 119 | defer iter.Release() 120 | for iter.Next() { 121 | keys = append(keys, string(iter.Key())) 122 | } 123 | return keys, nil 124 | } 125 | 126 | func DeleteThose(keys ...string) error { 127 | db, err := getDB() 128 | if err != nil { 129 | return err 130 | } 131 | t, err := db.OpenTransaction() 132 | defer t.Discard() 133 | if err != nil { 134 | return nil 135 | } 136 | var errSum error 137 | for _, key := range keys { 138 | err := t.Delete([]byte(key), nil) 139 | if err != nil { 140 | errSum = errors.Join(errSum, err) 141 | } 142 | } 143 | errSum = errors.Join(errSum, t.Commit()) 144 | return errSum 145 | } 146 | 147 | func FuzzySearch(key string) (string, error) { 148 | keys, err := All() 149 | if err != nil { 150 | return "", err 151 | } 152 | matches := fuzzy.Find(key, keys) 153 | if len(matches) > 0 { 154 | return matches[0].Str, nil 155 | } 156 | return key, nil 157 | } 158 | -------------------------------------------------------------------------------- /internal/item/fileinfo.go: -------------------------------------------------------------------------------- 1 | package item 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | "slices" 8 | 9 | "github.com/Equationzhao/g/internal/cached" 10 | "github.com/valyala/bytebufferpool" 11 | ) 12 | 13 | type FileInfo struct { 14 | os.FileInfo 15 | FullPath string 16 | Meta *cached.Map[string, Item] 17 | Cache map[string][]byte 18 | } 19 | 20 | type Option = func(info *FileInfo) error 21 | 22 | func WithSize(size int) Option { 23 | return func(info *FileInfo) error { 24 | info.Meta = cached.NewCacheMap[string, Item](size) 25 | return nil 26 | } 27 | } 28 | 29 | func WithFileInfo(info os.FileInfo) Option { 30 | return func(f *FileInfo) error { 31 | f.FileInfo = info 32 | return nil 33 | } 34 | } 35 | 36 | // WithPath will get the abs path of given string 37 | // and set the full path of FileInfo 38 | func WithPath(path string) Option { 39 | return func(f *FileInfo) error { 40 | abs, err := filepath.Abs(path) 41 | if err != nil { 42 | return err 43 | } 44 | f.FullPath = abs 45 | return nil 46 | } 47 | } 48 | 49 | func WithAbsPath(path string) Option { 50 | return func(f *FileInfo) error { 51 | f.FullPath = path 52 | return nil 53 | } 54 | } 55 | 56 | func NewFileInfoWithOption(opts ...Option) (*FileInfo, error) { 57 | f := &FileInfo{} 58 | var errSum error 59 | for _, opt := range opts { 60 | err := opt(f) 61 | if err != nil { 62 | errSum = errors.Join(errSum, err) 63 | } 64 | } 65 | if f.Meta == nil { 66 | f.Meta = cached.NewCacheMap[string, Item](20) 67 | } 68 | if f.Cache == nil { 69 | f.Cache = make(map[string][]byte) 70 | } 71 | return f, errSum 72 | } 73 | 74 | func NewFileInfo(name string) (*FileInfo, error) { 75 | info, err := os.Stat(name) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | abs, err := filepath.Abs(name) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return &FileInfo{ 86 | FileInfo: info, 87 | FullPath: abs, 88 | Meta: cached.NewCacheMap[string, Item](8), 89 | Cache: make(map[string][]byte), 90 | }, nil 91 | } 92 | 93 | // Keys return all keys in random order 94 | func (i *FileInfo) Keys() []string { 95 | items := i.Meta.Values() 96 | res := make([]string, 0, len(items)) 97 | for _, item := range items { 98 | res = append(res, item.String()) 99 | } 100 | return res 101 | } 102 | 103 | // KeysByOrder return Keys(ordered by No, ascending) 104 | func (i *FileInfo) KeysByOrder() []string { 105 | kNo := i.Meta.Pairs() 106 | 107 | slices.SortFunc( 108 | kNo, func(i, j cached.Pair[string, Item]) int { 109 | return i.Value().NO() - j.Value().NO() 110 | }, 111 | ) 112 | 113 | res := make([]string, 0, len(kNo)) 114 | for _, v := range kNo { 115 | res = append(res, v.Key()) 116 | } 117 | return res 118 | } 119 | 120 | // Del delete content by key 121 | func (i *FileInfo) Del(key string) { 122 | i.Meta.Del(key) 123 | } 124 | 125 | // Get content by key 126 | func (i *FileInfo) Get(key string) (Item, bool) { 127 | return i.Meta.Get(key) 128 | } 129 | 130 | // Set content by key 131 | func (i *FileInfo) Set(key string, ic Item) { 132 | i.Meta.Set(key, ic) 133 | } 134 | 135 | func (i *FileInfo) Values() []Item { 136 | return i.Meta.Values() 137 | } 138 | 139 | // ValuesByOrdered return all content (ordered by No, ascending) 140 | func (i *FileInfo) ValuesByOrdered() []Item { 141 | ics := i.Meta.Values() 142 | slices.SortFunc( 143 | ics, func(i, j Item) int { 144 | return i.NO() - j.NO() 145 | }, 146 | ) 147 | 148 | return ics 149 | } 150 | 151 | func (i *FileInfo) OrderedContent(delimiter string) string { 152 | res := bytebufferpool.Get() 153 | defer bytebufferpool.Put(res) 154 | items := i.ValuesByOrdered() 155 | for j, item := range items { 156 | _, _ = res.WriteString(item.String()) 157 | if j != len(items)-1 { 158 | _, _ = res.WriteString(delimiter) 159 | } 160 | } 161 | return res.String() 162 | } 163 | -------------------------------------------------------------------------------- /docs/ReleaseWorkflow.md: -------------------------------------------------------------------------------- 1 | # how to release 2 | 3 | ## requirement 4 | 5 | os: `macOS` or `linux` with `brew` 6 | 7 | for linux system, you can install the following dependencies through other pkg manager :-) 8 | 9 | software/toolchain: 10 | 11 | | name | how to install | remark | 12 | |---------------|------------------------------|-----------------------------------------------------------------------------------| 13 | | go >1.24.0 | `brew install go` | or use [go.dev](https://go.dev/dl/) / [goup](https://github.com/owenthereal/goup) | 14 | | git | `brew install git` | or use xcode version | 15 | | upx | `brew install upx` | | 16 | | dpkg-deb | `brew install dpkg` | | 17 | | gh | `brew install gh` | | 18 | | wget | `brew install wget` | | 19 | | gofumpt | `brew install gofumpt` | | 20 | | just | `brew install just` | | 21 | | prettier | `brew install prettier` | | 22 | | choose | `brew install choose-rust` | | 23 | | ripgrep | `brew install ripgrep` | | 24 | | shasum | | | 25 | | golangci-lint | `brew install golangci-lint` | | 26 | 27 | 28 | run the [script](../script/install_dev_requirement.sh) in the [script](../script) dir to install the dev requirement 29 | ```zsh 30 | ../script/install_dev_requirement.sh 31 | ``` 32 | 33 | ## pre-check 34 | 35 | - [ ] check code format and lint: `just precheck` 36 | - [ ] gen theme/doc file: `just gendocs` 37 | - [ ] run test: `just test` 38 | - [ ] check version: make sure the git tag and internal/cli/Version is the same. And git status is clean, git tag is at the current HEAD: `just check` 39 | 40 | ## build 41 | 42 | - [ ] generate release file: `just genrelease` 43 | 44 | or by steps: 45 | - [ ] cleanup: `just clean` 46 | - [ ] build: `just build` 47 | - [ ] compress: `just compress` 48 | - [ ] gen deb pkg: `just deb` 49 | - [ ] gen checksum: `just checksum` 50 | 51 | ## release 52 | 53 | - [ ] release: `just release` 54 | 55 | ## package manager 56 | 57 | ### AUR 58 | 59 | ssh://aur@aur.archlinux.org/g-ls.git 60 | make sure the aur repo is at '../g-ls' and 'Already up-to-date' 61 | 62 | ```zsh 63 | just aur 64 | ``` 65 | 66 | ### brew-tap 67 | 68 | git@github.com:Equationzhao/homebrew-g.git 69 | make sure the brew-tap repo is at '../homebrew-g' and 'Already up-to-date' 70 | 71 | ```zsh 72 | just brew-tap 73 | ``` 74 | 75 | ### brew-core 76 | 77 | usually brew-core will be automatically updated by the brew bot, but if you want to update it manually, you can try the following command 78 | 79 | ```zsh 80 | just brew 81 | ``` 82 | 83 | ### scoop 84 | 85 | ```zsh 86 | just scoop 87 | ``` 88 | 89 | the scoop manifest is at [scoop](../scoop/g.json) 90 | 91 | ```zsh 92 | git add -u && git commit -m 'ci: :construction_worker: update scoop' 93 | git push 94 | ``` 95 | 96 | if you have no access to push to the master branch, please push to another branch and make a pull request 97 | -------------------------------------------------------------------------------- /internal/cli/index.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/Equationzhao/g/internal/index" 10 | "github.com/Equationzhao/pathbeautify" 11 | "github.com/urfave/cli/v2" 12 | ) 13 | 14 | var indexFlags = []cli.Flag{ 15 | &cli.BoolFlag{ 16 | Name: "disable-index", 17 | Aliases: []string{"di", "no-update"}, 18 | Usage: "disable updating index", 19 | Category: "INDEX", 20 | DisableDefaultText: true, 21 | }, 22 | &cli.BoolFlag{ 23 | Name: "rebuild-index", 24 | Aliases: []string{"ri", "remove-all"}, 25 | Usage: "rebuild index", 26 | DisableDefaultText: true, 27 | Category: "INDEX", 28 | Action: func(context *cli.Context, b bool) error { 29 | if b { 30 | err := index.RebuildIndex() 31 | if err != nil { 32 | return err 33 | } 34 | } 35 | return Err4Exit{} 36 | }, 37 | }, 38 | &cli.BoolFlag{ 39 | Name: "fuzzy", 40 | Aliases: []string{"fz", "f"}, 41 | Usage: "fuzzy search", 42 | DisableDefaultText: true, 43 | Category: "INDEX", 44 | }, 45 | &cli.StringSliceFlag{ 46 | Name: "remove-index", 47 | Aliases: []string{"rm"}, 48 | Usage: "remove paths from index", 49 | Category: "INDEX", 50 | Action: func(context *cli.Context, i []string) error { 51 | var errSum error = nil 52 | 53 | beautification := true 54 | if context.Bool("np") { // --no-path-transform 55 | beautification = false 56 | } 57 | 58 | for _, s := range i { 59 | if beautification { 60 | s = pathbeautify.Transform(s) 61 | } 62 | 63 | // get absolute path 64 | r, err := filepath.Abs(s) 65 | if err != nil { 66 | errSum = errors.Join(errSum, fmt.Errorf("remove-path: %w", err)) 67 | continue 68 | } 69 | 70 | err = index.Delete(r) 71 | if err != nil { 72 | errSum = errors.Join(errSum, fmt.Errorf("remove-path: %w", err)) 73 | } 74 | } 75 | if errSum != nil { 76 | return errSum 77 | } 78 | return Err4Exit{} 79 | }, 80 | }, 81 | &cli.BoolFlag{ 82 | Name: "list-index", 83 | Aliases: []string{"li"}, 84 | Usage: "list index", 85 | DisableDefaultText: true, 86 | Category: "INDEX", 87 | Action: func(context *cli.Context, b bool) error { 88 | if b { 89 | keys, err := index.All() 90 | if err != nil { 91 | return err 92 | } 93 | for i := 0; i < len(keys); i++ { 94 | fmt.Println(keys[i]) 95 | } 96 | } 97 | return Err4Exit{} 98 | }, 99 | }, 100 | &cli.BoolFlag{ 101 | Name: "remove-current-path", 102 | Aliases: []string{"rcp", "rc", "rmc"}, 103 | Usage: "remove current path from index", 104 | Category: "INDEX", 105 | DisableDefaultText: true, 106 | Action: func(context *cli.Context, b bool) error { 107 | if b { 108 | r, err := os.Getwd() 109 | if err != nil { 110 | return err 111 | } 112 | err = index.Delete(r) 113 | if err != nil { 114 | return err 115 | } 116 | } 117 | return Err4Exit{} 118 | }, 119 | }, 120 | &cli.BoolFlag{ 121 | Name: "remove-invalid-path", 122 | Aliases: []string{"rip"}, 123 | Usage: "remove invalid paths from index", 124 | Category: "INDEX", 125 | DisableDefaultText: true, 126 | Action: func(ctx *cli.Context, b bool) error { 127 | if b { 128 | paths, err := index.All() 129 | if err != nil { 130 | return err 131 | } 132 | invalid := make([]string, 0, len(paths)) 133 | for _, path := range paths { 134 | _, err := os.Stat(path) 135 | if err != nil { 136 | invalid = append(invalid, path) 137 | } 138 | } 139 | err = index.DeleteThose(invalid...) 140 | if err != nil { 141 | return err 142 | } 143 | } 144 | return Err4Exit{} 145 | }, 146 | }, 147 | } 148 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | This Contribution Guidelines is modified from the [GitHub doc](https://github.com/github/docs/) project 4 | 5 | Feel free to ask any questions in the [Discussions QA](https://github.com/Equationzhao/g/discussions/categories/q-a) 6 | 7 | ## Issues 8 | 9 | ### Create a new issue 10 | 11 | If you have the following ideas 12 | 13 | - spot a problem with the project 🐛 14 | - have any suggestions 📈 15 | - want a new style 💄 16 | 17 | [search if an issue already exists](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments). \ 18 | If a related issue doesn't exist, you can open a new issue using a relevant [issue form](https://github.com/Equationzhao/g/issues/new/choose). 19 | 20 | 21 | ### Solve an issue 22 | 23 | Scan through our [existing issues](https://github.com/Equationzhao/g/issues) to find one that interests you. You can narrow down the search using `labels` as filters. 24 | If you find an issue to work on, you are welcome to open a PR with a fix. 25 | 26 | 27 | ## Make changes 28 | 29 | 1. Fork and Clone the Repository 30 | ```shell 31 | git clone git@github.com:yourname/g.git 32 | ``` 33 | 2. Make sure you have installed **go**, and go version >= 1.24.0 . 34 | ```shell 35 | go version 36 | ``` 37 | 3. Make your changes! 38 | 39 | ## Commit messages 40 | It's recommended to follow the commit style below: 41 | ```text 42 | [optional scope]: 43 | 44 | [optional body] 45 | ``` 46 | 47 | ```text 48 | fix: 49 | feat: 50 | build: 51 | chore: 52 | ci: 53 | docs: 54 | style: 55 | refactor: 56 | perf: 57 | test: 58 | ... 59 | ``` 60 | Also, you can use [gitmoji](https://gitmoji.dev) in the commit message 61 | 62 | Examples are provided to illustrate the recommended style: 63 | ```text 64 | style: :lipstick: change color for 'readme' file 65 | 66 | change from BrightYellow to Yellow 67 | ``` 68 | 69 | ## Tests 70 | 71 | Please refer to the [TestWorkflow](docs/TestWorkflow.md) 72 | 73 | ## PR 74 | 75 | When you're finished with the changes, create a pull request, also known as a PR.\ 76 | Before you submit your pr, please check out the following stuffs 77 | 78 | - [ ] you have run `go mod tidy` and `gofumpt --extra -w -l .` 79 | - [ ] your code has the necessary comments and documentation (if needed) 80 | - [ ] you have written tests for your changes (if needed) 81 | - [ ] Pass other tests, ***if you're breaking the tests, please explain why in the PR description*** 82 | 83 | and when you're submitting your pr, make sure to: 84 | - Fill the "Ready for review" template so that we can review your PR. This template helps reviewers understand your changes as well as the purpose of your pull request. 85 | - Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one. 86 | - Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge. 87 | Once you submit your PR, maintainers will review your proposal. We may ask questions or request additional information. 88 | - We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch. 89 | - As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations). 90 | - If you run into any merge issues, checkout this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to help you resolve merge conflicts and other issues. 91 | 92 | ## Release 93 | 94 | Please refer to the [ReleaseWorkflow](docs/ReleaseWorkflow.md) -------------------------------------------------------------------------------- /internal/content/name_test.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "io/fs" 7 | "os" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/Equationzhao/g/internal/item" 12 | "github.com/agiledragon/gomonkey/v2" 13 | "github.com/zeebo/assert" 14 | ) 15 | 16 | func TestName_checkDereferenceErr(t *testing.T) { 17 | tests := []struct { 18 | name string 19 | err error 20 | wantSymlinks string 21 | panic bool 22 | }{ 23 | { 24 | name: "not path PathError", 25 | err: errors.New("not path PathError"), 26 | wantSymlinks: "not path PathError", 27 | }, 28 | { 29 | name: "path PathError", 30 | err: &fs.PathError{Op: "lstat", Path: "nowhere", Err: errors.New("no such file or directory")}, 31 | wantSymlinks: "nowhere", 32 | }, 33 | { 34 | name: "nil", 35 | err: nil, 36 | panic: true, 37 | }, 38 | } 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | defer func() { 42 | if r := recover(); r != nil { 43 | if !tt.panic { 44 | t.Errorf("checkDereferenceErr() panic = %v", r) 45 | } 46 | } 47 | }() 48 | var n Name 49 | if gotSymlinks := n.checkDereferenceErr(tt.err); gotSymlinks != tt.wantSymlinks { 50 | t.Errorf("checkDereferenceErr() = %v, want %v", gotSymlinks, tt.wantSymlinks) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | // TODO gomonkey seems not working ? 57 | // func TestMountsOn(t *testing.T) { 58 | // patch := gomonkey.NewPatches() 59 | // defer patch.Reset() 60 | // patch.ApplyFunc(disk.Partitions, func(all bool) ([]disk.PartitionStat, error) { 61 | // return []disk.PartitionStat{ 62 | // { 63 | // Device: "/dev/sda1", 64 | // Mountpoint: "/", 65 | // Fstype: "apfs", 66 | // Opts: []string{"rw", "relatime"}, 67 | // }, 68 | // { 69 | // Device: "/devfs", 70 | // Mountpoint: "/dev", 71 | // Fstype: "apfs", 72 | // Opts: []string{"rw", "relatime"}, 73 | // }, 74 | // }, nil 75 | // }) 76 | // tests := []struct { 77 | // name string 78 | // path string 79 | // want string 80 | // }{ 81 | // { 82 | // name: "root", 83 | // path: "/", 84 | // want: "[/dev/sda1 (apfs)]", 85 | // }, 86 | // { 87 | // name: "dev", 88 | // path: "/dev", 89 | // want: "[/devfs (apfs)]", 90 | // }, 91 | // { 92 | // "not found", 93 | // "/notfound", 94 | // "", 95 | // }, 96 | // } 97 | // for _, tt := range tests { 98 | // t.Run(tt.name, func(t *testing.T) { 99 | // if got := MountsOn(tt.path); got != tt.want { 100 | // t.Errorf("MountsOn() = %v, want %v", got, tt.want) 101 | // } 102 | // }) 103 | // } 104 | // } 105 | 106 | func TestStatistics_MarshalJSON(t *testing.T) { 107 | type fields struct { 108 | file uint64 109 | dir uint64 110 | link uint64 111 | } 112 | tests := []struct { 113 | name string 114 | fields fields 115 | want []byte 116 | wantErr error 117 | }{ 118 | { 119 | name: "ok", 120 | fields: fields{ 121 | file: 100, 122 | dir: 111, 123 | link: 123, 124 | }, 125 | want: []byte(`{"File":100,"Dir":111,"Link":123}`), 126 | wantErr: nil, 127 | }, 128 | } 129 | for _, tt := range tests { 130 | t.Run(tt.name, func(t *testing.T) { 131 | s := &Statistics{} 132 | s.file.Add(tt.fields.file) 133 | s.dir.Add(tt.fields.dir) 134 | s.link.Add(tt.fields.link) 135 | got, err := s.MarshalJSON() 136 | if !errors.Is(err, tt.wantErr) { 137 | t.Errorf("MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) 138 | return 139 | } 140 | if !reflect.DeepEqual(got, tt.want) { 141 | t.Errorf("MarshalJSON() got = %v, want %v", string(got), string(tt.want)) 142 | } 143 | }) 144 | } 145 | } 146 | 147 | func Test_checkIfEmpty(t *testing.T) { 148 | patch := gomonkey.ApplyFuncReturn(os.Open, nil, io.EOF) 149 | defer patch.Reset() 150 | 151 | i := item.FileInfo{ 152 | FullPath: "test", 153 | } 154 | assert.True(t, checkIfEmpty(&i)) 155 | 156 | patch.ApplyFuncReturn(os.Open, nil, io.ErrUnexpectedEOF) 157 | assert.True(t, checkIfEmpty(&i)) 158 | } 159 | -------------------------------------------------------------------------------- /internal/util/jaro.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Felipe da Cunha Gonçalves 3 | All Rights Reserved. 4 | 5 | MIT LICENSE 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the "Software"), to deal in 9 | the Software without restriction, including without limitation the rights to 10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software is furnished to do so, 12 | subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 21 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | // copied from github.com/xrash/smetrics 26 | // fix transpositions calculation 27 | 28 | package util 29 | 30 | import "math" 31 | 32 | func JaroWinkler(a, b string, boostThreshold float64, prefixSize int) float64 { 33 | j := jaro(a, b) 34 | 35 | if j <= boostThreshold { 36 | return j 37 | } 38 | 39 | prefixSize = min(len(a), len(b), prefixSize) 40 | 41 | var prefixMatch float64 42 | for i := 0; i < prefixSize; i++ { 43 | if a[i] == b[i] { 44 | prefixMatch++ 45 | } else { 46 | break 47 | } 48 | } 49 | 50 | return j + 0.1*prefixMatch*(1.0-j) 51 | } 52 | 53 | // The Jaro distance. The result is 1 for equal strings, and 0 for completely different strings. 54 | func jaro(a, b string) float64 { 55 | // If both strings are zero-length, they are completely equal, 56 | // therefore return 1. 57 | if len(a) == 0 && len(b) == 0 { 58 | return 1 59 | } 60 | 61 | // If one string is zero-length, strings are completely different, 62 | // therefore return 0. 63 | if len(a) == 0 || len(b) == 0 { 64 | return 0 65 | } 66 | 67 | // Define the necessary variables for the algorithm. 68 | la := float64(len(a)) 69 | lb := float64(len(b)) 70 | matchRange := int(math.Max(0, math.Floor(math.Max(la, lb)/2.0)-1)) 71 | matchesA := make([]bool, len(a)) 72 | matchesB := make([]bool, len(b)) 73 | var matches float64 = 0 74 | 75 | // Step 1: Matches 76 | // Loop through each character of the first string, 77 | // looking for a matching character in the second string. 78 | for i := 0; i < len(a); i++ { 79 | start := int(math.Max(0, float64(i-matchRange))) 80 | end := int(math.Min(lb-1, float64(i+matchRange))) 81 | 82 | for j := start; j <= end; j++ { 83 | if matchesB[j] { 84 | continue 85 | } 86 | 87 | if a[i] == b[j] { 88 | matchesA[i] = true 89 | matchesB[j] = true 90 | matches++ 91 | break 92 | } 93 | } 94 | } 95 | 96 | // If there are no matches, strings are completely different, 97 | // therefore return 0. 98 | if matches == 0 { 99 | return 0 100 | } 101 | 102 | // Step 2: Transpositions 103 | // Loop through the matches' arrays, looking for 104 | // unaligned matches. Count the number of unaligned matches. 105 | unaligned := 0 106 | j := 0 107 | for i := 0; i < len(a); i++ { 108 | if !matchesA[i] { 109 | continue 110 | } 111 | 112 | for !matchesB[j] { 113 | j++ 114 | } 115 | 116 | if a[i] != b[j] { 117 | unaligned++ 118 | } 119 | 120 | j++ 121 | } 122 | 123 | // The number of unaligned matches divided by two, is the number of _transpositions_. 124 | transpositions := math.Floor(float64(unaligned) / 2) 125 | 126 | // Jaro distance is the average between these three numbers: 127 | // 1. matches / length of string A 128 | // 2. matches / length of string B 129 | // 3. (matches - transpositions/matches) 130 | // So, all that divided by three is the final result. 131 | return ((matches / la) + (matches / lb) + ((matches - transpositions) / matches)) / 3.0 132 | } 133 | -------------------------------------------------------------------------------- /internal/theme/color_test.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | constval "github.com/Equationzhao/g/internal/global" 8 | "github.com/gookit/color" 9 | ) 10 | 11 | func TestHexToRgb(t *testing.T) { 12 | type args struct { 13 | hex string 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | wantRgb []uint8 19 | }{ 20 | { 21 | name: "black", 22 | args: args{hex: "#000000"}, 23 | wantRgb: []uint8{0, 0, 0}, 24 | }, 25 | { 26 | name: "white", 27 | args: args{hex: "#ffffff"}, 28 | wantRgb: []uint8{255, 255, 255}, 29 | }, 30 | { 31 | name: "red", 32 | args: args{hex: "#ff0000"}, 33 | wantRgb: []uint8{255, 0, 0}, 34 | }, 35 | { 36 | name: "3 digits", 37 | args: args{hex: "#f00"}, 38 | wantRgb: []uint8{255, 0, 0}, 39 | }, 40 | { 41 | name: "#0x", 42 | args: args{hex: "#0xff0000"}, 43 | wantRgb: []uint8{255, 0, 0}, 44 | }, 45 | { 46 | name: "empty", 47 | args: args{hex: ""}, 48 | wantRgb: []uint8{0, 0, 0}, 49 | }, 50 | { 51 | name: "invalid", 52 | args: args{hex: "invalid"}, 53 | wantRgb: []uint8{0, 0, 0}, 54 | }, 55 | } 56 | for _, tt := range tests { 57 | t.Run(tt.name, func(t *testing.T) { 58 | if gotRgb := HexToRgb(tt.args.hex); !reflect.DeepEqual(gotRgb, tt.wantRgb) { 59 | t.Errorf("HexToRgb() = %v, want %v", gotRgb, tt.wantRgb) 60 | } 61 | }) 62 | } 63 | } 64 | 65 | func TestBasicConverts(t *testing.T) { 66 | for c := range basicColor2str { 67 | _, _, _ = BasicToRGBInt(c) 68 | _ = BasicToRGB(c) 69 | _ = BasicTo256(c) 70 | } 71 | } 72 | 73 | func Test256Converts(t *testing.T) { 74 | for i := 0; i < 256; i++ { 75 | _ = c256ToBasic(uint8(i)) 76 | _ = c256ToRGB(uint8(i)) 77 | } 78 | } 79 | 80 | func TestRGBConverts(t *testing.T) { 81 | for r := 0; r < 10; r++ { 82 | for g := 0; g < 10; g++ { 83 | for b := 0; b < 10; b++ { 84 | _ = RGBToBasic(uint8(r), uint8(g), uint8(b)) 85 | _ = RGBTo256(uint8(r), uint8(g), uint8(b)) 86 | } 87 | } 88 | } 89 | } 90 | 91 | func TestConvertColorIfGreaterThanExpect(t *testing.T) { 92 | type args struct { 93 | to color.Level 94 | src string 95 | } 96 | tests := []struct { 97 | name string 98 | args args 99 | want string 100 | wantErr bool 101 | }{ 102 | { 103 | name: "none", 104 | args: args{to: None, src: constval.Black}, 105 | want: "", 106 | wantErr: false, 107 | }, 108 | { 109 | name: "basic2basic", 110 | args: args{to: Ascii, src: constval.Black}, 111 | want: constval.Black, 112 | wantErr: false, 113 | }, 114 | { 115 | name: "basicTo256", 116 | args: args{to: C256, src: constval.Black}, 117 | want: constval.Black, 118 | wantErr: false, 119 | }, 120 | { 121 | name: "basicToRGB", 122 | args: args{to: TrueColor, src: constval.Black}, 123 | want: constval.Black, 124 | wantErr: false, 125 | }, 126 | { 127 | name: "256toBasic", 128 | args: args{to: Ascii, src: color256(100)}, 129 | want: c256ToBasic(100), 130 | wantErr: false, 131 | }, 132 | { 133 | name: "256to256", 134 | args: args{to: C256, src: color256(100)}, 135 | want: color256(100), 136 | }, 137 | { 138 | name: "256toRGB", 139 | args: args{to: TrueColor, src: color256(100)}, 140 | want: color256(100), 141 | }, 142 | { 143 | name: "RGBtoBasic", 144 | args: args{to: Ascii, src: rgb(100, 200, 255)}, 145 | want: RGBToBasic(100, 200, 255), 146 | }, 147 | { 148 | name: "RGBto256", 149 | args: args{to: C256, src: rgb(100, 200, 255)}, 150 | want: RGBTo256(100, 200, 255), 151 | }, 152 | { 153 | name: "RGBtoRGB", 154 | args: args{to: TrueColor, src: rgb(100, 200, 255)}, 155 | want: rgb(100, 200, 255), 156 | }, 157 | { 158 | name: "unknown", 159 | args: args{to: 100, src: constval.Black}, 160 | want: constval.Black, 161 | }, 162 | } 163 | for _, tt := range tests { 164 | t.Run(tt.name, func(t *testing.T) { 165 | got, err := ConvertColorIfGreaterThanExpect(tt.args.to, tt.args.src) 166 | if (err != nil) != tt.wantErr { 167 | t.Errorf("ConvertColorIfGreaterThanExpect() error = %v, wantErr %v", err, tt.wantErr) 168 | return 169 | } 170 | if got != tt.want { 171 | t.Errorf("ConvertColorIfGreaterThanExpect() got = %v, want %v", got, tt.want) 172 | } 173 | }) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "runtime/debug" 8 | "slices" 9 | "strings" 10 | 11 | "github.com/Equationzhao/g/internal/cli" 12 | "github.com/Equationzhao/g/internal/config" 13 | debugSetting "github.com/Equationzhao/g/internal/global/debug" 14 | "github.com/Equationzhao/g/internal/global/doc" 15 | "github.com/Equationzhao/g/internal/util" 16 | "github.com/Equationzhao/g/man" 17 | ucli "github.com/urfave/cli/v2" 18 | ) 19 | 20 | func main() { 21 | // catch panic and print stack trace and version info 22 | defer func() { 23 | if !debugSetting.Enable { 24 | catchPanic(recover()) 25 | } 26 | }() 27 | // when build with tag `doc`, generate md and man file 28 | if doc.Enable { 29 | man.GenMan() 30 | } else { 31 | preprocessArgs() 32 | err := cli.G.Run(os.Args) 33 | if err != nil { 34 | if !errors.Is(err, cli.Err4Exit{}) { 35 | if cli.ReturnCode == 0 { 36 | cli.ReturnCode = 1 37 | } 38 | _, _ = fmt.Fprintln(os.Stderr, cli.MakeErrorStr(err.Error())) 39 | } 40 | } 41 | } 42 | } 43 | 44 | func catchPanic(err any) { 45 | if err != nil { 46 | _, _ = fmt.Fprintf(os.Stderr, "Version: v%s\n", cli.Version) 47 | _, _ = fmt.Fprintf(os.Stderr, "Please file an issue at %s with the following panic info\n\n", util.MakeLink("https://github.com/Equationzhao/g/issues/new/choose", "Github Repo")) 48 | _, _ = fmt.Fprintln(os.Stderr, cli.MakeErrorStr(fmt.Sprintf("error message:\n%v\n", err))) 49 | _, _ = fmt.Fprintln(os.Stderr, cli.MakeErrorStr(fmt.Sprintf("stack trace:\n%s", debug.Stack()))) 50 | cli.ReturnCode = 1 51 | } 52 | os.Exit(cli.ReturnCode) 53 | } 54 | 55 | func preprocessArgs() { 56 | rearrangeArgs() 57 | // normal logic 58 | // load config if the args do not contains -no-config 59 | if !slices.ContainsFunc(os.Args, hasNoConfig) { 60 | defaultArgs, err := config.Load() 61 | // if successfully load config and **the config.Args do not contain -no-config** 62 | if err == nil && !slices.ContainsFunc(defaultArgs.Args, hasNoConfig) { 63 | os.Args = slices.Insert(os.Args, 1, defaultArgs.Args...) 64 | } else if err != nil { // if failed to load config 65 | // if it's read error 66 | var errReadConfig config.ErrReadConfig 67 | if errors.As(err, &errReadConfig) { 68 | _, _ = fmt.Fprintln(os.Stderr, cli.MakeErrorStr(err.Error())) 69 | } 70 | } 71 | } else { 72 | // contains -no-config 73 | // remove it before the cli.G starts 74 | os.Args = slices.DeleteFunc(os.Args, hasNoConfig) 75 | } 76 | } 77 | 78 | func rearrangeArgs() { 79 | if len(os.Args) <= 2 { 80 | return 81 | } 82 | flags, paths := separateArgs(os.Args[1:]) 83 | newArgs := append([]string{os.Args[0]}, append(flags, paths...)...) 84 | os.Args = newArgs 85 | } 86 | 87 | func separateArgs(args []string) (flags, paths []string) { 88 | flagsWithArgs := buildFlagsWithArgsMap() 89 | expectValue, hasDoubleDash := false, false 90 | for i := 0; i < len(args); i++ { 91 | arg := args[i] 92 | if arg == "--" { 93 | hasDoubleDash = true 94 | if i+1 < len(args) { 95 | paths = append(paths, args[i+1]) 96 | i++ 97 | } 98 | continue 99 | } 100 | if strings.HasPrefix(arg, "--") { 101 | i = handleLongFlag(arg, args, i, &flags, &expectValue, flagsWithArgs) 102 | } else if strings.HasPrefix(arg, "-") { 103 | i = handleShortFlag(arg, args, i, &flags, &expectValue, flagsWithArgs) 104 | } else { 105 | if expectValue { 106 | flags = append(flags, arg) 107 | expectValue = false 108 | } else { 109 | paths = append(paths, arg) 110 | } 111 | } 112 | } 113 | if hasDoubleDash { 114 | flags = append(flags, "--") 115 | } 116 | return flags, paths 117 | } 118 | 119 | func buildFlagsWithArgsMap() map[string]bool { 120 | flagsWithArgs := make(map[string]bool) 121 | for _, flag := range cli.G.Flags { 122 | switch flag.(type) { 123 | case *ucli.BoolFlag: 124 | for _, s := range flag.Names() { 125 | flagsWithArgs[s] = false 126 | } 127 | default: 128 | for _, s := range flag.Names() { 129 | flagsWithArgs[s] = true 130 | } 131 | } 132 | } 133 | return flagsWithArgs 134 | } 135 | 136 | func handleLongFlag(arg string, args []string, i int, flags *[]string, expectValue *bool, flagsWithArgs map[string]bool) int { 137 | parts := strings.SplitN(arg, "=", 2) 138 | flagName := strings.TrimPrefix(parts[0], "--") 139 | *flags = append(*flags, arg) 140 | 141 | if len(parts) == 2 || !flagsWithArgs[flagName] { 142 | return i 143 | } 144 | 145 | if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { 146 | *expectValue = true 147 | } 148 | return i 149 | } 150 | 151 | func handleShortFlag(arg string, args []string, i int, flags *[]string, expectValue *bool, flagsWithArgs map[string]bool) int { 152 | parts := strings.SplitN(arg, "=", 2) 153 | flagName := strings.TrimPrefix(parts[0], "-") 154 | *flags = append(*flags, arg) 155 | if len(parts) == 2 || !flagsWithArgs[flagName] { 156 | return i 157 | } 158 | if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { 159 | *expectValue = true 160 | } 161 | return i 162 | } 163 | 164 | func hasNoConfig(s string) bool { 165 | if s == "-no-config" || s == "--no-config" { 166 | return true 167 | } 168 | return false 169 | } 170 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build-macos-latest: 14 | runs-on: macos-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: 1.24.0 22 | 23 | - name: Build linux 386 24 | run: CGO_ENABLED=0 GOOS=linux GOARCH=386 go build 25 | - name: Build linux amd64 26 | run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build 27 | - name: Build linux arm 28 | run: CGO_ENABLED=0 GOOS=linux GOARCH=arm go build 29 | - name: Build linux arm64 30 | run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build 31 | - name: Build linux loong64 32 | run: CGO_ENABLED=0 GOOS=linux GOARCH=loong64 go build 33 | - name: Build darwin amd64 34 | run: CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build 35 | - name: Build darwin arm64 36 | run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build 37 | - name: Build windows 386 38 | run: CGO_ENABLED=0 GOOS=windows GOARCH=386 go build 39 | - name: Build windows amd64 40 | run: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build 41 | - name: Build windows arm64 42 | run: CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build 43 | - name: Build windows arm 44 | run: CGO_ENABLED=0 GOOS=windows GOARCH=arm go build 45 | - name: Run tests 46 | run: go test -v ./... 47 | - name: Check size of darwin amd64 binary 48 | run: | 49 | SIZE=$(stat -f%z g) 50 | echo "Binary size is $SIZE bytes" 51 | if [ $SIZE -gt 10000000 ]; then 52 | echo "Binary size exceeds 10MB" 53 | fi 54 | 55 | build-macos-13: 56 | runs-on: macos-latest 57 | steps: 58 | - uses: actions/checkout@v4 59 | 60 | - name: Set up Go 61 | uses: actions/setup-go@v5 62 | with: 63 | go-version: 1.24.0 64 | 65 | - name: Build linux 386 66 | run: CGO_ENABLED=0 GOOS=linux GOARCH=386 go build 67 | - name: Build linux amd64 68 | run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build 69 | - name: Build linux arm 70 | run: CGO_ENABLED=0 GOOS=linux GOARCH=arm go build 71 | - name: Build linux arm64 72 | run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build 73 | - name: Build linux loong64 74 | run: CGO_ENABLED=0 GOOS=linux GOARCH=loong64 go build 75 | - name: Build darwin amd64 76 | run: CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build 77 | - name: Build darwin arm64 78 | run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build 79 | - name: Build windows 386 80 | run: CGO_ENABLED=0 GOOS=windows GOARCH=386 go build 81 | - name: Build windows amd64 82 | run: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build 83 | - name: Build windows arm64 84 | run: CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build 85 | - name: Build windows arm 86 | run: CGO_ENABLED=0 GOOS=windows GOARCH=arm go build 87 | - name: Run tests 88 | run: go test -v ./... 89 | - name: Check size of darwin amd64 binary 90 | run: | 91 | SIZE=$(stat -f%z g) 92 | echo "Binary size is $SIZE bytes" 93 | if [ $SIZE -gt 10000000 ]; then 94 | echo "Binary size exceeds 10MB" 95 | fi 96 | 97 | windows-latest: 98 | runs-on: windows-latest 99 | steps: 100 | - uses: actions/checkout@v4 101 | 102 | - name: Set up Go 103 | uses: actions/setup-go@v5 104 | with: 105 | go-version: 1.24.0 106 | 107 | - name: Build 108 | run: go build 109 | - name: Check size of windows binary 110 | shell: bash 111 | run: | 112 | SIZE=$(stat -c%s g.exe) 113 | echo "Binary size is $SIZE bytes" 114 | if [ $SIZE -gt 10000000 ]; then 115 | echo "Binary size exceeds 10MB" 116 | fi 117 | 118 | ubuntu-latest-latest: 119 | runs-on: ubuntu-latest 120 | steps: 121 | - uses: actions/checkout@v4 122 | 123 | - name: Set up Go 124 | uses: actions/setup-go@v5 125 | with: 126 | go-version: 1.24.0 127 | 128 | - name: Build linux 386 129 | run: CGO_ENABLED=0 GOOS=linux GOARCH=386 go build 130 | - name: Build linux amd64 131 | run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build 132 | - name: Build linux arm 133 | run: CGO_ENABLED=0 GOOS=linux GOARCH=arm go build 134 | - name: Build linux arm64 135 | run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build 136 | - name: Build linux loong64 137 | run: CGO_ENABLED=0 GOOS=linux GOARCH=loong64 go build 138 | - name: Build windows 386 139 | run: CGO_ENABLED=0 GOOS=windows GOARCH=386 go build 140 | - name: Build windows amd64 141 | run: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build 142 | - name: Build windows arm64 143 | run: CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build 144 | - name: Build windows arm 145 | run: CGO_ENABLED=0 GOOS=windows GOARCH=arm go build 146 | - name: Run tests 147 | run: go test -v ./... 148 | - name: Check size of linux binary 149 | run: | 150 | SIZE=$(stat -c%s g) 151 | echo "Binary size is $SIZE bytes" 152 | if [ $SIZE -gt 10000000 ]; then 153 | echo "Binary size exceeds 10MB" 154 | fi 155 | -------------------------------------------------------------------------------- /internal/content/git.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | "github.com/Equationzhao/g/internal/align" 8 | "github.com/Equationzhao/g/internal/git" 9 | constval "github.com/Equationzhao/g/internal/global" 10 | "github.com/Equationzhao/g/internal/item" 11 | "github.com/Equationzhao/g/internal/render" 12 | "github.com/alphadose/haxmap" 13 | ) 14 | 15 | type GitEnabler struct { 16 | cache git.Cache 17 | Path git.RepoPath 18 | } 19 | 20 | func (g *GitEnabler) InitCache(repo git.RepoPath) { 21 | g.cache.Set(repo, git.DefaultInit(repo)()) 22 | } 23 | 24 | func NewGitEnabler() *GitEnabler { 25 | return &GitEnabler{ 26 | cache: git.GetCache(), 27 | } 28 | } 29 | 30 | func (g *GitEnabler) Enable(renderer *render.Renderer) ContentOption { 31 | isOrIsParentOf := func(parent, child string) bool { 32 | if parent == child { 33 | return true 34 | } 35 | if strings.HasPrefix(child, parent+string(filepath.Separator)) { 36 | return true 37 | } 38 | return false 39 | } 40 | 41 | return func(info *item.FileInfo) (string, string) { 42 | gits, ok := g.cache.Get(g.Path) 43 | if ok { 44 | topLevel, err := git.GetTopLevel(g.Path) 45 | if err != nil { 46 | return gitByName(git.Unmodified, renderer) + gitByName(git.Unmodified, renderer), GitStatus 47 | } 48 | rel, err := filepath.Rel(topLevel, info.FullPath) 49 | if err != nil { 50 | return gitByName(git.Unmodified, renderer) + gitByName(git.Unmodified, renderer), GitStatus 51 | } 52 | for _, status := range *gits { 53 | if status.X == git.Ignored || status.Y == git.Ignored { 54 | // if status is ignored, 55 | // and the file is or is a child of the ignored file 56 | if isOrIsParentOf(status.Name, rel) { 57 | return gitByName(status.X, renderer) + gitByName(status.Y, renderer), GitStatus 58 | } 59 | } else { 60 | if isOrIsParentOf(rel, status.Name) { 61 | return gitByName(status.X, renderer) + gitByName(status.Y, renderer), GitStatus 62 | } 63 | } 64 | } 65 | } 66 | return gitByName(git.Unmodified, renderer) + gitByName(git.Unmodified, renderer), GitStatus 67 | } 68 | } 69 | 70 | func gitByName(status git.Status, renderer *render.Renderer) string { 71 | switch status { 72 | case git.Unmodified: 73 | return renderer.GitUnmodified("-") 74 | case git.Modified: 75 | return renderer.GitModified("M") 76 | case git.Added: 77 | return renderer.GitAdded("A") 78 | case git.Deleted: 79 | return renderer.GitDeleted("D") 80 | case git.Renamed: 81 | return renderer.GitRenamed("R") 82 | case git.Copied: 83 | return renderer.GitCopied("C") 84 | case git.Untracked: 85 | return renderer.GitUntracked("?") 86 | case git.Ignored: 87 | return renderer.GitIgnored("!") 88 | case git.TypeChanged: 89 | return renderer.GitTypeChanged("T") 90 | case git.UpdatedButUnmerged: 91 | return renderer.GitUpdatedButUnmerged("U") 92 | default: 93 | return "" 94 | } 95 | } 96 | 97 | const ( 98 | GitStatus = constval.NameOfGitStatus 99 | GitRepoBranch = constval.NameOfGitRepoBranch 100 | GitRepoStatus = constval.NameOfGitRepoStatus 101 | GitCommitHash = constval.NameOfGitCommitHash 102 | GitAuthor = constval.NameOfGitAuthor 103 | GitAuthorDate = constval.NameOfGitAuthorDate 104 | ) 105 | 106 | type GitRepoEnabler struct{} 107 | 108 | func (g *GitRepoEnabler) Enable(renderer *render.Renderer) ContentOption { 109 | align.Register(GitRepoBranch) 110 | return func(info *item.FileInfo) (string, string) { 111 | // get branch name 112 | return renderer.GitRepoBranch(git.GetBranch(info.FullPath)), GitRepoBranch 113 | } 114 | } 115 | 116 | func (g *GitRepoEnabler) EnableStatus(renderer *render.Renderer) ContentOption { 117 | align.Register(GitRepoStatus) 118 | return func(info *item.FileInfo) (string, string) { 119 | // get repo status 120 | return renderer.GitRepoStatus(git.GetRepoStatus(info.FullPath)), GitRepoStatus 121 | } 122 | } 123 | 124 | func NewGitRepoEnabler() *GitRepoEnabler { 125 | return &GitRepoEnabler{} 126 | } 127 | 128 | type GitCommitEnabler struct { 129 | Cache *haxmap.Map[string, git.CommitInfo] 130 | } 131 | 132 | func (g *GitCommitEnabler) init(info *item.FileInfo) git.CommitInfo { 133 | m, ok := g.Cache.Get(info.FullPath) 134 | if ok { 135 | return m 136 | } 137 | commit, err := git.GetLastCommitInfo(info.FullPath) 138 | if err != nil { 139 | commit = &git.NoneCommitInfo 140 | } 141 | defer g.Cache.Set(info.FullPath, *commit) 142 | return *commit 143 | } 144 | 145 | func (g *GitCommitEnabler) EnableHash(renderer *render.Renderer) ContentOption { 146 | return func(info *item.FileInfo) (string, string) { 147 | commit := g.init(info) 148 | return renderer.GitCommitHash(commit.Hash), GitCommitHash 149 | } 150 | } 151 | 152 | func (g *GitCommitEnabler) EnableAuthor(renderer *render.Renderer) ContentOption { 153 | return func(info *item.FileInfo) (string, string) { 154 | commit := g.init(info) 155 | return renderer.GitAuthor(commit.Author), GitAuthor 156 | } 157 | } 158 | 159 | func (g *GitCommitEnabler) EnableAuthorDateWithTimeFormat(renderer *render.Renderer, timeFormat string) ContentOption { 160 | return func(info *item.FileInfo) (string, string) { 161 | commit := g.init(info) 162 | return renderer.GitAuthorDate(commit.GetAuthorDateInFormat(timeFormat)), GitAuthorDate 163 | } 164 | } 165 | 166 | func NewGitCommitEnabler() *GitCommitEnabler { 167 | return &GitCommitEnabler{ 168 | Cache: haxmap.New[string, git.CommitInfo](10), 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /internal/content/duplicate.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "fmt" 7 | "hash" 8 | "hash/crc32" 9 | "io" 10 | "os" 11 | "slices" 12 | "strings" 13 | 14 | "github.com/Equationzhao/g/internal/cached" 15 | "github.com/Equationzhao/g/internal/item" 16 | "github.com/Equationzhao/g/internal/util" 17 | ) 18 | 19 | type ( 20 | filenameList = *util.Slice[string] 21 | hashStr = string 22 | ) 23 | 24 | type DuplicateDetect struct { 25 | IsThrough bool 26 | hashTb *cached.Map[hashStr, filenameList] 27 | } 28 | 29 | type DOption func(d *DuplicateDetect) 30 | 31 | const defaultTbSize = 200 32 | 33 | func NewDuplicateDetect(options ...DOption) *DuplicateDetect { 34 | d := &DuplicateDetect{} 35 | 36 | for _, option := range options { 37 | option(d) 38 | } 39 | 40 | if d.hashTb == nil { 41 | d.hashTb = cached.NewCacheMap[hashStr, filenameList](defaultTbSize) 42 | } 43 | 44 | return d 45 | } 46 | 47 | func DuplicateWithTbSize(size int) DOption { 48 | return func(d *DuplicateDetect) { 49 | d.hashTb = cached.NewCacheMap[hashStr, filenameList](size) 50 | } 51 | } 52 | 53 | func DetectorFallthrough(d *DuplicateDetect) { 54 | d.IsThrough = true 55 | } 56 | 57 | func (d *DuplicateDetect) Enable() NoOutputOption { 58 | job := make(chan *item.FileInfo, 100) 59 | isJobFinished := cached.NewCacheMap[string, *chan struct{}](1000) 60 | go func() { 61 | for info := range job { 62 | func() { 63 | c, _ := isJobFinished.Get(info.FullPath) 64 | defer func() { 65 | *c <- struct{}{} 66 | }() 67 | afterHash, err := fileHash(info, d.IsThrough) 68 | if err != nil { 69 | return 70 | } 71 | actual, _ := d.hashTb.GetOrCompute( 72 | afterHash, func() filenameList { 73 | return util.NewSlice[string](10) 74 | }, 75 | ) 76 | actual.AppendTo(info.Name()) 77 | }() 78 | } 79 | }() 80 | return func(info *item.FileInfo) { 81 | c := make(chan struct{}, 1) 82 | isJobFinished.Set(info.FullPath, &c) 83 | job <- info 84 | <-c 85 | } 86 | } 87 | 88 | type Duplicate struct { 89 | Filenames []string 90 | } 91 | 92 | func (d *DuplicateDetect) Result() []Duplicate { 93 | list := d.hashTb.Values() 94 | res := make([]Duplicate, 0, len(list)) 95 | for _, i := range list { 96 | if l := i.Len(); l > 1 { 97 | f := i.GetCopy() 98 | slices.SortStableFunc(f, func(a, b string) int { 99 | return strings.Compare(a, b) 100 | }) 101 | res = append(res, Duplicate{Filenames: f}) 102 | } 103 | } 104 | return res 105 | } 106 | 107 | func (d *DuplicateDetect) Reset() { 108 | d.hashTb.ForEach(func(k string, v *util.Slice[string]) bool { 109 | v.Clear() 110 | return true 111 | }) 112 | } 113 | 114 | func (d *DuplicateDetect) Fprint(w io.Writer) { 115 | r := d.Result() 116 | if len(r) != 0 { 117 | _, _ = fmt.Fprintln(w, "Duplicates:") 118 | for _, i := range r { 119 | for _, filename := range i.Filenames { 120 | _, _ = fmt.Fprint(w, " ", filename) 121 | } 122 | _, _ = fmt.Fprintln(w) 123 | } 124 | } 125 | } 126 | 127 | var thresholdFileSize = int64(16 * KiB) 128 | 129 | // fileHash calculates the hash of the file provided. 130 | // If isThorough is true, then it uses SHA256 of the entire file. 131 | // Otherwise, it uses CRC32 of "crucial bytes" of the file. 132 | func fileHash(fileInfo *item.FileInfo, isThorough bool) (string, error) { 133 | if !fileInfo.Mode().IsRegular() { 134 | return "", fmt.Errorf("can't compute hash of non-regular file") 135 | } 136 | var prefix string 137 | var bytes []byte 138 | var fileReadErr error 139 | if isThorough || fileInfo.Size() <= thresholdFileSize { 140 | bytes, fileReadErr = os.ReadFile(fileInfo.FullPath) 141 | if fileReadErr != nil { 142 | return "", fmt.Errorf("couldn't read file: %w", fileReadErr) 143 | } 144 | if fileInfo.Size() <= thresholdFileSize { 145 | prefix = "f" 146 | } 147 | } else { 148 | prefix = "s" 149 | bytes, fileReadErr = readCrucialBytes(fileInfo.FullPath, fileInfo.Size()) 150 | if fileReadErr != nil { 151 | return "", fmt.Errorf("couldn't calculate hash: %w", fileReadErr) 152 | } 153 | } 154 | var h hash.Hash 155 | if isThorough { 156 | h = sha256.New() 157 | } else { 158 | h = crc32.NewIEEE() 159 | } 160 | _, hashErr := h.Write(bytes) 161 | if hashErr != nil { 162 | return "", fmt.Errorf("error while computing hash: %w", hashErr) 163 | } 164 | hashBytes := h.Sum(nil) 165 | return prefix + hex.EncodeToString(hashBytes), nil 166 | } 167 | 168 | // readCrucialBytes reads the first few bytes, middle bytes and last few bytes of the file 169 | func readCrucialBytes(filePath string, fileSize int64) ([]byte, error) { 170 | file, err := os.Open(filePath) 171 | if err != nil { 172 | return nil, err 173 | } 174 | defer file.Close() 175 | 176 | firstBytes := make([]byte, thresholdFileSize/2) 177 | _, fErr := file.ReadAt(firstBytes, 0) 178 | if fErr != nil { 179 | return nil, fmt.Errorf("couldn't read first few bytes (maybe file is corrupted?): %w", fErr) 180 | } 181 | middleBytes := make([]byte, thresholdFileSize/4) 182 | _, mErr := file.ReadAt(middleBytes, fileSize/2) 183 | if mErr != nil { 184 | return nil, fmt.Errorf("couldn't read middle bytes (maybe file is corrupted?): %w", mErr) 185 | } 186 | lastBytes := make([]byte, thresholdFileSize/4) 187 | _, lErr := file.ReadAt(lastBytes, fileSize-thresholdFileSize/4) 188 | if lErr != nil { 189 | return nil, fmt.Errorf("couldn't read end bytes (maybe file is corrupted?): %w", lErr) 190 | } 191 | bytes := append(append(firstBytes, middleBytes...), lastBytes...) 192 | return bytes, nil 193 | } 194 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | equationzhao@foxmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /internal/util/termlink.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2022 Skyascii 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | // code from github.com/savioxavier/termlink 26 | 27 | package util 28 | 29 | import ( 30 | "fmt" 31 | "os" 32 | ) 33 | 34 | // v struct represents a semver version (usually, with some exceptions) 35 | // with major, minor, and patch segments 36 | type v struct { 37 | major int 38 | minor int 39 | patch int 40 | } 41 | 42 | // parseVersion takes a string "version" number and returns 43 | // a Version struct with the major, minor, and patch 44 | // segments parsed from the string. 45 | // If a version number is not provided 46 | func parseVersion(version string) v { 47 | var major, minor, patch int 48 | _, _ = fmt.Sscanf(version, "%d.%d.%d", &major, &minor, &patch) 49 | return v{ 50 | major: major, 51 | minor: minor, 52 | patch: patch, 53 | } 54 | } 55 | 56 | // hasEnvironmentVariables returns true if the environment variable "name" 57 | // is present in the environment, false otherwise 58 | func hasEnv(name string) bool { 59 | _, envExists := os.LookupEnv(name) 60 | 61 | return envExists 62 | } 63 | 64 | // checkAllEnvs returns true if any of the environment variables in the "vars" 65 | // string slice are actually present in the environment, false otherwise 66 | func checkAllEnvs(vars []string) bool { 67 | for _, v := range vars { 68 | if hasEnv(v) { 69 | return true 70 | } 71 | } 72 | 73 | return false 74 | } 75 | 76 | // getEnv returns the value of the environment variable, if it exists 77 | func getEnv(name string) string { 78 | envValue, _ := os.LookupEnv(name) 79 | 80 | return envValue 81 | } 82 | 83 | // matchesEnv returns true if the environment variable "name" matches any 84 | // of the given values in the "values" string slice, false otherwise 85 | func matchesEnv(name string, values []string) bool { 86 | if hasEnv(name) { 87 | for _, value := range values { 88 | if getEnv(name) == value { 89 | return true 90 | } 91 | } 92 | } 93 | return false 94 | } 95 | 96 | func SupportsHyperlinks() bool { 97 | // Allow hyperlinks to be forced, independent of any environment variables 98 | // Instead of checking whether it is equal to anything other than "0", 99 | // a set of allowed values are provided, as something like 100 | // FORCE_HYPERLINK="do-not-enable-it" wouldn't make sense if it returned true 101 | if matchesEnv("FORCE_HYPERLINK", []string{"1", "true", "always", "enabled"}) { 102 | return true 103 | } 104 | 105 | // VTE-based terminals (Gnome Terminal, Guake, ROXTerm, etc) 106 | // VTE_VERSION is rendered as four-digit version string 107 | // eg: 0.52.2 => 5202 108 | // parseVersion will parse it with a standalone major segment 109 | // with minor and patch segments set to 0 110 | // 0.50.0 (parsed as 5000) was supposed to support hyperlinks, but throws a segfault 111 | // so we check if the "major" version is greater than 5000 (5000 exclusive) 112 | if hasEnv("VTE_VERSION") { 113 | v := parseVersion(getEnv("VTE_VERSION")) 114 | return v.major > 5000 115 | } 116 | 117 | // Terminals which have a TERM_PROGRAM variable set 118 | // This is the most versatile environment variable as it also provides another 119 | // variable called TERM_PROGRAM_VERSION, which helps us to determine 120 | // the exact version of the program, and allow for stricter variable checks 121 | if hasEnv("TERM_PROGRAM") { 122 | v := parseVersion(getEnv("TERM_PROGRAM_VERSION")) 123 | 124 | switch term := getEnv("TERM_PROGRAM"); term { 125 | case "iTerm.app": 126 | if v.major == 3 { 127 | return v.minor >= 1 128 | } 129 | return v.major > 3 130 | case "WezTerm": 131 | // Even though WezTerm's version is something like 20200620-160318-e00b076c 132 | // parseVersion will still parse it with a standalone major segment (ie: 20200620) 133 | // with minor and patch segments set to 0 134 | return v.major >= 20200620 135 | case "vscode": 136 | return v.major > 1 || (v.major == 1 && v.minor >= 72) 137 | case "ghostty": 138 | // It is unclear when during the private beta that ghostty started supporting hyperlinks, 139 | // so we'll start from the public release. 140 | return v.major >= 1 141 | 142 | // Hyper Terminal used to be included in this list, and it even supports hyperlinks 143 | // but the hyperlinks are pseudo-hyperlinks and are actually not clickable 144 | } 145 | } 146 | 147 | // Terminals which have a TERM variable set 148 | if matchesEnv("TERM", []string{"xterm-kitty", "alacritty", "alacritty-direct", "xterm-ghostty"}) { 149 | return true 150 | } 151 | 152 | // Terminals which have a COLORTERM variable set 153 | if matchesEnv("COLORTERM", []string{"xfce4-terminal"}) { 154 | return true 155 | } 156 | 157 | // Terminals in JetBrains IDEs 158 | if matchesEnv("TERMINAL_EMULATOR", []string{"JetBrains-JediTerm"}) { 159 | return true 160 | } 161 | 162 | // Match standalone environment variables 163 | // ie, those which do not require any special handling 164 | // or version checking 165 | if checkAllEnvs([]string{ 166 | "DOMTERM", 167 | "WT_SESSION", 168 | "KONSOLE_VERSION", 169 | }) { 170 | return true 171 | } 172 | 173 | return false 174 | } 175 | -------------------------------------------------------------------------------- /internal/git/git_status.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "bufio" 5 | "os/exec" 6 | "strings" 7 | 8 | "github.com/Equationzhao/g/internal/util" 9 | "github.com/Equationzhao/pathbeautify" 10 | ) 11 | 12 | // FileGit is an entry name with git status 13 | // the name will not end with file separator 14 | type FileGit struct { 15 | Name string 16 | X, Y Status 17 | } 18 | 19 | /* 20 | Set sets the status of the file based on the XY string 21 | X Y Meaning 22 | ------------------------------------------------- 23 | 24 | [AMD] not updated 25 | 26 | M [ MTD] updated in index 27 | T [ MTD] type changed in index 28 | A [ MTD] added to index 29 | D deleted from index 30 | R [ MTD] renamed in index 31 | C [ MTD] copied in index 32 | [MTARC] index and work tree matches 33 | [ MTARC] M work tree changed since index 34 | [ MTARC] T type changed in work tree since index 35 | [ MTARC] D deleted in work tree 36 | 37 | // R renamed in work tree 38 | // C copied in work tree 39 | 40 | ------------------------------------------------- 41 | D D unmerged, both deleted 42 | A U unmerged, added by us 43 | U D unmerged, deleted by them 44 | U A unmerged, added by them 45 | D U unmerged, deleted by us 46 | A A unmerged, both added 47 | U U unmerged, both modified 48 | ------------------------------------------------- 49 | ? ? untracked 50 | ! ! ignored 51 | ------------------------------------------------- 52 | */ 53 | func (f *FileGit) Set(XY string) { 54 | set := func(s *Status, c byte) { 55 | *s = Byte2Status(c) 56 | } 57 | set(&f.X, XY[0]) 58 | set(&f.Y, XY[1]) 59 | } 60 | 61 | type FileGits = []FileGit 62 | 63 | type RepoPath = string 64 | 65 | // GetShortGitStatus read the git status of the repository located at the path 66 | func GetShortGitStatus(repoPath RepoPath) (string, error) { 67 | c := exec.Command("git", "status", "-s", "--ignored", "--porcelain", repoPath) 68 | c.Dir = repoPath 69 | out, err := c.Output() 70 | if err == nil { 71 | return string(out), err 72 | } 73 | return "", err 74 | } 75 | 76 | type Status uint8 77 | 78 | const ( 79 | Unknown Status = iota 80 | Unmodified // - 81 | Modified // M 82 | Added // A 83 | Deleted // D 84 | Renamed // R 85 | Copied // C 86 | Untracked // ? 87 | Ignored // ! 88 | TypeChanged // T 89 | UpdatedButUnmerged // U 90 | ) 91 | 92 | // ParseShort parses a git status output command 93 | // It is compatible with the short version of the git status command 94 | // modified from https://le-gall.bzh/post/go/parsing-git-status-with-go/ author: Sébastien Le Gall 95 | func ParseShort(r string) (res FileGits) { 96 | s := bufio.NewScanner(strings.NewReader(r)) 97 | 98 | for s.Scan() { 99 | // Skip any empty line 100 | if len(s.Text()) < 1 { 101 | continue 102 | } 103 | break 104 | } 105 | 106 | fg := FileGit{} 107 | for { 108 | str := s.Text() 109 | if len(str) < 1 { 110 | continue 111 | } 112 | status := str[0:2] 113 | fg.Set(status) 114 | if fg.X == Renamed || fg.Y == Renamed || fg.X == Copied || fg.Y == Copied { 115 | // origin -> rename 116 | // the actual file name is rename 117 | o2r := str[3:] 118 | fg.Name = util.RemoveSep(pathbeautify.CleanSeparator(o2r[strings.Index(o2r, " -> ")+4:])) 119 | } else { 120 | fg.Name = util.RemoveSep(pathbeautify.CleanSeparator(str[3:])) 121 | } 122 | 123 | res = append(res, fg) 124 | if !s.Scan() { 125 | break 126 | } 127 | } 128 | 129 | return res 130 | } 131 | 132 | func (s Status) String() string { 133 | switch s { 134 | case Modified: 135 | return "M" 136 | case Added: 137 | return "A" 138 | case Deleted: 139 | return "D" 140 | case Renamed: 141 | return "R" 142 | case Copied: 143 | return "C" 144 | case Untracked: 145 | return "?" 146 | case Ignored: 147 | return "!" 148 | case Unmodified: 149 | return "-" 150 | case TypeChanged: 151 | return "T" 152 | case UpdatedButUnmerged: 153 | return "U" 154 | case Unknown: 155 | return "^" 156 | } 157 | return "^" 158 | } 159 | 160 | func Byte2Status(c byte) Status { 161 | switch c { 162 | case 'M': 163 | return Modified 164 | case 'A': 165 | return Added 166 | case 'D': 167 | return Deleted 168 | case 'R': 169 | return Renamed 170 | case 'C': 171 | return Copied 172 | case '?': 173 | return Untracked 174 | case '!': 175 | return Ignored 176 | case '-', ' ': 177 | return Unmodified 178 | case 'T': 179 | return TypeChanged 180 | case 'U': 181 | return UpdatedButUnmerged 182 | case '^': 183 | return Unknown 184 | } 185 | return Unknown 186 | } 187 | 188 | func (r RepoStatus) String() string { 189 | switch r { 190 | case RepoStatusClean: 191 | return "+" 192 | case RepoStatusDirty: 193 | return "|" 194 | case RepoStatusSkip: 195 | return "" 196 | } 197 | return "" 198 | } 199 | 200 | const ( 201 | RepoStatusSkip RepoStatus = iota 202 | RepoStatusClean 203 | RepoStatusDirty 204 | ) 205 | 206 | type RepoStatus uint8 207 | 208 | // GetBranch returns the branch of the repository 209 | // only return the branch when the path is the root of the repository 210 | func GetBranch(repoPath RepoPath) string { 211 | if root, _ := GetTopLevel(repoPath); root != repoPath { 212 | return "" 213 | } 214 | 215 | c := exec.Command("git", "branch", "--show-current") 216 | c.Dir = repoPath 217 | out, err := c.Output() 218 | if err == nil { 219 | return strings.TrimSpace(string(out)) 220 | } 221 | return "" 222 | } 223 | 224 | // GetRepoStatus returns the status of the repository 225 | // only return the status when the path is the root of the repository 226 | func GetRepoStatus(repoPath RepoPath) RepoStatus { 227 | if root, _ := GetTopLevel(repoPath); root != repoPath { 228 | return RepoStatusSkip 229 | } 230 | 231 | c := exec.Command("git", "status", "--porcelain") 232 | c.Dir = repoPath 233 | out, err := c.Output() 234 | if err == nil { 235 | if len(out) == 0 { 236 | return RepoStatusClean 237 | } 238 | return RepoStatusDirty 239 | } 240 | return RepoStatusSkip 241 | } 242 | -------------------------------------------------------------------------------- /completions/fish/g.fish: -------------------------------------------------------------------------------- 1 | function __fish_g_no_subcommand 2 | return (commandline -opc)[1] = 'g' 3 | end 4 | 5 | complete -c g -l bug -d "report bug" 6 | complete -c g -l duplicate -d "show duplicate files" -a "(__fish_complete_path)" 7 | complete -c g -l no-config -d "do not load config file" 8 | complete -c g -l no-path-transform -d "disable path transformation" 9 | complete -c g -l help -s h -s \? -d "show help" 10 | complete -c g -l version -s v -d "print the version" 11 | complete -c g -s '#' -d "print entry number for each entry" 12 | complete -c g -l csv -d "output in csv format" 13 | complete -c g -l tsv -d "output in tsv format" 14 | complete -c g -l byline -s 1 -d "print by line" 15 | complete -c g -l classic -d "enable classic mode" 16 | complete -c g -l color -d "set terminal color mode" -a "always auto never basic 256 24bit" 17 | complete -c g -l colorless -d "without color" 18 | complete -c g -l depth -d "limit recursive/tree depth" -r -f 19 | complete -c g -l format -d "set output format" -a "across commas horizontal long single-column verbose vertical table markdown csv tsv json tree" 20 | complete -c g -l flags -d "list file flags" -r -f 21 | complete -c g -l file-type -d "do not append indicator to file types" 22 | complete -c g -l md -d "output in markdown-table format" 23 | complete -c g -l markdown -d "output in markdown-table format" 24 | complete -c g -l table -d "output in table format" 25 | complete -c g -l table-style -d "set table style" -a "ascii unicode" 26 | complete -c g -l term-width -d "set screen width" -r -f 27 | complete -c g -l theme -d "apply theme" -a "(__fish_complete_path)" 28 | complete -c g -l tree-style -d "set tree style" -a "ascii unicode rectangle" 29 | complete -c g -l zero -s 0 -d "end each output line with NUL" 30 | complete -c g -s C -d "list entries by columns" 31 | complete -c g -s F -d "append indicator to entries" 32 | complete -c g -s R -d "recurse into directories" 33 | complete -c g -s T -d "recursively list in tree" 34 | complete -c g -s d -d "list directories themselves" 35 | complete -c g -s j -d "output in json format" 36 | complete -c g -s m -d "fill width with a comma separated list" 37 | complete -c g -s x -d "list entries by lines" 38 | complete -c g -l init -d "show the init script for shell" -a "zsh bash fish powershell nushell" 39 | complete -c g -l sort -d "sort by field" -a "nature none name Name size Size time Time owner Owner group Group extension Extension inode Inode width Width mime Mime" 40 | complete -c g -l dir-first -d "list directories before files" 41 | complete -c g -l group-directories-first -d "list directories before files" 42 | complete -c g -l reverse -s r -d "reverse the order of the sort" 43 | complete -c g -l versionsort -d "sort by version numbers" 44 | complete -c g -s S -d "sort by file size" 45 | complete -c g -l si -d "use powers of 1000 not 1024 for size format" 46 | complete -c g -l sizesort -d "sort by file size" 47 | complete -c g -s U -l no-sort -d "do not sort" 48 | complete -c g -s X -l sort-by-ext -d "sort alphabetically by entry extension" 49 | complete -c g -l accessed -d "accessed time" 50 | complete -c g -l all -d "show all info/use a long listing format" 51 | complete -c g -l birth -d "birth time" 52 | complete -c g -l blocks -d "show block size" 53 | complete -c g -l charset -d "show charset of text file" 54 | complete -c g -l checksum -d "show checksum of file" 55 | complete -c g -l checksum-algorithm -d "checksum algorithm" -a "md5 sha1 sha224 sha256 sha384 sha512 crc32" 56 | complete -c g -l created -d "created time" 57 | complete -c g -l dereference -d "dereference symbolic links" 58 | complete -c g -l footer -d "add a footer row" 59 | complete -c g -l full-path -d "show full path" 60 | complete -c g -l full-time -d "like -all/l --time-style=full-iso" -a "default iso long-iso full-iso +FORMAT" 61 | complete -c g -l gid -d "show gid instead of groupname" 62 | complete -c g -l git -l git-status -d "show git status" 63 | complete -c g -l git-repo-branch -l branch -d "list root of git-tree branch" 64 | complete -c g -l git-repo-status -l repo-status -d "list root of git-tree status" 65 | complete -c g -l group -d "show group" 66 | complete -c g -l header -l title -d "add a header row" 67 | complete -c g -l hyperlink -d "attach hyperlink to filenames" -a "auto always never" 68 | complete -c g -l icon -d "show icon" 69 | complete -c g -l inode -s i -d "show inode" 70 | complete -c g -l mime -d "show mime file type" 71 | complete -c g -l mime-parent -d "show mime parent type" 72 | complete -c g -l modified -d "modified time" 73 | complete -c g -l mounts -d "show mount details" 74 | complete -c g -l no-dereference -d "do not follow symbolic links" 75 | complete -c g -l no-icon -d "disable icon" 76 | complete -c g -l no-total-size -d "disable total size" 77 | complete -c g -l numeric -l numeric-uid-gid -d "show numeric user and group IDs" 78 | complete -c g -l octal-perm -d "list permission in octal format" 79 | complete -c g -l owner -d "show owner" 80 | complete -c g -l perm -d "show permission" 81 | complete -c g -l recursive-size -d "show recursive size of dir" 82 | complete -c g -l relative-to -d "show relative path" -a "(__fish_complete_path)" 83 | complete -c g -l relative-time -d "show relative time" 84 | complete -c g -l size -d "show file/dir size" 85 | complete -c g -l size-unit -l block-size -d "set size unit" -a "bit \"bit\" b \"byte\" k m g t p e z y bb nb auto" 86 | complete -c g -l smart-group -d "only show group if different from owner" 87 | complete -c g -l statistic -d "show statistic info" 88 | complete -c g -l stdin -d "read path from stdin, split by newline" 89 | complete -c g -l time -d "show time" 90 | complete -c g -l time-style -d "set time/date format" -r -f 91 | complete -c g -l time-type -d "set time type" -a "mod \"modified\" create access all birth" 92 | complete -c g -l total-size -d "show total size" 93 | complete -c g -l uid -d "show uid instead of username" 94 | complete -c g -s G -l no-group -d "do not print group names in a long listing" 95 | complete -c g -s H -l link -d "list number of hard links for each file" 96 | complete -c g -s N -l literal -d "print entry names without quoting" 97 | complete -c g -s O -l no-owner -d "do not print owner names in a long listing" 98 | complete -c g -s Q -l quote-name -d "enclose entry names in double quotes" 99 | complete -c g -s g -d "like -all, but do not list owner" 100 | complete -c g -s l -l long -d "use a long listing format" 101 | complete -c g -s o -d "like -all, but do not list group information" 102 | complete -c g -f -a "(__fish_complete_path)" 103 | -------------------------------------------------------------------------------- /completions/zsh/_g: -------------------------------------------------------------------------------- 1 | #compdef g 2 | 3 | _g() { 4 | local -a options 5 | local expl 6 | 7 | options=( 8 | '--bug[report bug]' 9 | '--duplicate[show duplicate files]:duplicate files:_files' 10 | '--no-config[do not load config file]' 11 | '--no-path-transform[disable path transformation]' 12 | '--[check if there is new release]' 13 | '--help[show help]' 14 | '-h[show help]' 15 | '-?[show help]' 16 | '--version[print the version]' 17 | '-v[print the version]' 18 | '-#[print entry number for each entry]' 19 | '--csv[output in csv format]' 20 | '--tsv[output in tsv format]' 21 | '--byline[print by line]' 22 | '-1[print by line]' 23 | '--classic[enable classic mode]' 24 | '--color[set terminal color mode]:color mode:((always auto never basic 256 24bit))' 25 | '--colorless[without color]' 26 | '--depth[limit recursive/tree depth]:depth:' 27 | '--format[set output format]:format:((across commas horizontal long single-column verbose vertical table markdown csv tsv json tree))' 28 | '--file-type[do not append indicator to file types]' 29 | '--md[output in markdown-table format]' 30 | '--markdown[output in markdown-table format]' 31 | '--table[output in table format]' 32 | '--table-style[set table style]:style:((ascii unicode))' 33 | '--term-width[set screen width]:width:' 34 | '--theme[apply theme]:path to theme:_files -/' 35 | '--tree-style[set tree style]:style:((ascii unicode rectangle))' 36 | '--zero[end each output line with NUL]' 37 | '-0[end each output line with NUL]' 38 | '-C[list entries by columns]' 39 | '-F[append indicator to entries]' 40 | '-R[recurse into directories]' 41 | '-T[recursively list in tree]' 42 | '-d[list directories themselves]' 43 | '-j[output in json format]' 44 | '-m[fill width with a comma separated list]' 45 | '-x[list entries by lines]' 46 | '--init[show the init script for shell]:shell:((zsh bash fish powershell nushell))' 47 | '--sort[sort by field]:field:((nature none name Name size Size time Time owner Owner group Group extension Extension inode Inode width Width mime Mime))' 48 | '--dir-first[list directories before files]' 49 | '--group-directories-first[list directories before files]' 50 | '--reverse[reverse the order of the sort]' 51 | '-r[reverse the order of the sort]' 52 | '--versionsort[sort by version numbers]' 53 | '-S[sort by file size]' 54 | '--si[use powers of 1000 not 1024 for size format]' 55 | '--sizesort[sort by file size]' 56 | '-U[do not sort]' 57 | '--no-sort[do not sort]' 58 | '-X[sort alphabetically by entry extension]' 59 | '--sort-by-ext[sort alphabetically by entry extension]' 60 | '--accessed[accessed time]' 61 | '--all[show all info/use a long listing format]' 62 | '--birth[birth time]' 63 | '--blocks[show block size]' 64 | '--charset[show charset of text file]' 65 | '--checksum[show checksum of file]' 66 | '--checksum-algorithm[checksum algorithm]:algorithm:((md5 sha1 sha224 sha256 sha384 sha512 crc32))' 67 | '--created[created time]' 68 | '--dereference[dereference symbolic links]' 69 | '--footer[add a footer row]' 70 | '--full-path[show full path]' 71 | '--full-time[like -all/l --time-style=full-iso]:time-style:((default iso long-iso full-iso +FORMAT))' 72 | '--flags[list file flags]' 73 | '--gid[show gid instead of groupname]' 74 | '--git[show git status]' 75 | '--git-status[show git status]' 76 | '--git-repo-branch[list root of git-tree branch]' 77 | '--branch[list root of git-tree branch]' 78 | '--git-repo-status[list root of git-tree status]' 79 | '--repo-status[list root of git-tree status]' 80 | '--group[show group]' 81 | '--header[add a header row]' 82 | '--title[add a header row]' 83 | '--hyperlink[attach hyperlink to filenames]:mode:((auto always never))' 84 | '--icon[show icon]' 85 | '--inode[show inode]' 86 | '-i[show inode]' 87 | '--mime[show mime file type]' 88 | '--mime-parent[show mime parent type]' 89 | '--modified[modified time]' 90 | '--mounts[show mount details]' 91 | '--no-dereference[do not follow symbolic links]' 92 | '--no-icon[disable icon]' 93 | '--no-total-size[disable total size]' 94 | '--numeric[show numeric user and group IDs]' 95 | '--numeric-uid-gid[show numeric user and group IDs]' 96 | '--octal-perm[list permission in octal format]' 97 | '--owner[show owner]' 98 | '--perm[show permission]' 99 | '--recursive-size[show recursive size of dir]' 100 | '--relative-to[show relative path]:path:_files -/' 101 | '--relative-time[show relative time]' 102 | '--size[show file/dir size]' 103 | '--size-unit[set size unit]:unit:((bit "bit" b "byte" k m g t p e z y bb nb auto))' 104 | '--block-size[set size unit]:unit:((bit "bit" b "byte" k m g t p e z y bb nb auto))' 105 | '--smart-group[only show group if different from owner]' 106 | '--statistic[show statistic info]' 107 | '--stdin[read path from stdin, split by newline]' 108 | '--time[show time]' 109 | '--time-style[set time/date format]:format:' 110 | '--time-type[set time type]:type:((mod "modified" create access all birth))' 111 | '--total-size[show total size]' 112 | '--uid[show uid instead of username]' 113 | '-G[don not print group names in a long listing]' 114 | '--no-group[don not print group names in a long listing]' 115 | '-H[list number of hard links for each file]' 116 | '--link[list number of hard links for each file]' 117 | '-N[print entry names without quoting]' 118 | '--literal[print entry names without quoting]' 119 | '-O[don not print owner names in a long listing]' 120 | '--no-owner[don not print owner names in a long listing]' 121 | '-Q[enclose entry names in double quotes]' 122 | '--quote-name[enclose entry names in double quotes]' 123 | '-g[like -all, but do not list owner]' 124 | '-l[use a long listing format]' 125 | '--long[use a long listing format]' 126 | '-o[like -all, but do not list group information]' 127 | '*:filename:_files' 128 | ) 129 | 130 | _arguments -s $options 131 | } 132 | 133 | _g -------------------------------------------------------------------------------- /internal/osbased/usergroup_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package osbased 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | "unsafe" 9 | 10 | "github.com/Equationzhao/g/internal/item" 11 | ) 12 | 13 | /* 14 | MIT License 15 | 16 | Copyright (c) 2019 Andrew Carlson 17 | 18 | Permission is hereby granted, free of charge, to any person obtaining a copy 19 | of this software and associated documentation files (the "Software"), to deal 20 | in the Software without restriction, including without limitation the rights 21 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 22 | copies of the Software, and to permit persons to whom the Software is 23 | furnished to do so, subject to the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be included in all 26 | copies or substantial portions of the Software. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | SOFTWARE. 35 | */ 36 | 37 | var ( 38 | libadvapi32 = syscall.NewLazyDLL("advapi32.dll") 39 | procGetFileSecurity = libadvapi32.NewProc("GetFileSecurityW") 40 | procGetSecurityDescriptorOwner = libadvapi32.NewProc("GetSecurityDescriptorOwner") 41 | ) 42 | 43 | func Group(info os.FileInfo) string { 44 | path := info.Name() 45 | if info.(*item.FileInfo).FullPath != "" { 46 | path = info.(*item.FileInfo).FullPath 47 | } 48 | 49 | var needed uint32 50 | fromString, err := syscall.UTF16PtrFromString(path) 51 | if err != nil { 52 | return "unknown" 53 | } 54 | _, _, _ = procGetFileSecurity.Call( 55 | uintptr(unsafe.Pointer(fromString)), 56 | 0x00000001, /* OWNER_SECURITY_INFORMATION */ 57 | 0, 58 | 0, 59 | uintptr(unsafe.Pointer(&needed)), 60 | ) 61 | 62 | if needed == 0 { 63 | return "unknown" 64 | } 65 | 66 | buf := make([]byte, needed) 67 | r1, _, err := procGetFileSecurity.Call( 68 | uintptr(unsafe.Pointer(fromString)), 69 | 0x00000001, /* OWNER_SECURITY_INFORMATION */ 70 | uintptr(unsafe.Pointer(&buf[0])), 71 | uintptr(needed), 72 | uintptr(unsafe.Pointer(&needed)), 73 | ) 74 | if r1 == 0 && err != nil { 75 | return "unknown" 76 | } 77 | var ownerDefaulted uint32 78 | var sid *syscall.SID 79 | r1, _, err = procGetSecurityDescriptorOwner.Call( 80 | uintptr(unsafe.Pointer(&buf[0])), 81 | uintptr(unsafe.Pointer(&sid)), 82 | uintptr(unsafe.Pointer(&ownerDefaulted)), 83 | ) 84 | if r1 == 0 && err != nil { 85 | return "unknown" 86 | } 87 | _, name, _, err := sid.LookupAccount("") 88 | if r1 == 0 && err != nil { 89 | return "unknown" 90 | } 91 | if name == "" { 92 | return "unknown" 93 | } 94 | return name 95 | } 96 | 97 | func Owner(info os.FileInfo) string { 98 | path := info.Name() 99 | if info.(*item.FileInfo).FullPath != "" { 100 | path = info.(*item.FileInfo).FullPath 101 | } 102 | var needed uint32 103 | fromString, err := syscall.UTF16PtrFromString(path) 104 | if err != nil { 105 | return "unknown" 106 | } 107 | _, _, _ = procGetFileSecurity.Call( 108 | uintptr(unsafe.Pointer(fromString)), 109 | 0x00000001, /* OWNER_SECURITY_INFORMATION */ 110 | 0, 111 | 0, 112 | uintptr(unsafe.Pointer(&needed)), 113 | ) 114 | buf := make([]byte, needed) 115 | 116 | if needed == 0 { 117 | return "unknown" 118 | } 119 | 120 | r1, _, err := procGetFileSecurity.Call( 121 | uintptr(unsafe.Pointer(fromString)), 122 | 0x00000001, /* OWNER_SECURITY_INFORMATION */ 123 | uintptr(unsafe.Pointer(&buf[0])), 124 | uintptr(needed), 125 | uintptr(unsafe.Pointer(&needed)), 126 | ) 127 | if r1 == 0 && err != nil { 128 | return "unknown" 129 | } 130 | var ownerDefaulted uint32 131 | var sid *syscall.SID 132 | r1, _, err = procGetSecurityDescriptorOwner.Call( 133 | uintptr(unsafe.Pointer(&buf[0])), 134 | uintptr(unsafe.Pointer(&sid)), 135 | uintptr(unsafe.Pointer(&ownerDefaulted)), 136 | ) 137 | if r1 == 0 && err != nil { 138 | return "unknown" 139 | } 140 | name, _, _, err := sid.LookupAccount("") 141 | if r1 == 0 && err != nil { 142 | return "unknown" 143 | } 144 | if name == "" { 145 | return "unknown" 146 | } 147 | return name 148 | } 149 | 150 | func OwnerID(a os.FileInfo) string { 151 | path := a.Name() 152 | if a.(*item.FileInfo).FullPath != "" { 153 | path = a.(*item.FileInfo).FullPath 154 | } 155 | fromString, err := syscall.UTF16PtrFromString(path) 156 | if err != nil { 157 | return "unknown" 158 | } 159 | var needed uint32 160 | _, _, _ = procGetFileSecurity.Call( 161 | uintptr(unsafe.Pointer(fromString)), 162 | 0x00000001, /* OWNER_SECURITY_INFORMATION */ 163 | 0, 164 | 0, 165 | uintptr(unsafe.Pointer(&needed)), 166 | ) 167 | buf := make([]byte, needed) 168 | 169 | if needed == 0 { 170 | return "unknown" 171 | } 172 | 173 | r1, _, err := procGetFileSecurity.Call( 174 | uintptr(unsafe.Pointer(fromString)), 175 | 0x00000001, /* OWNER_SECURITY_INFORMATION */ 176 | uintptr(unsafe.Pointer(&buf[0])), 177 | uintptr(needed), 178 | uintptr(unsafe.Pointer(&needed)), 179 | ) 180 | if r1 == 0 && err != nil { 181 | return "unknown" 182 | } 183 | var ownerDefaulted uint32 184 | var sid *syscall.SID 185 | r1, _, err = procGetSecurityDescriptorOwner.Call( 186 | uintptr(unsafe.Pointer(&buf[0])), 187 | uintptr(unsafe.Pointer(&sid)), 188 | uintptr(unsafe.Pointer(&ownerDefaulted)), 189 | ) 190 | if r1 == 0 && err != nil { 191 | return "unknown" 192 | } 193 | s, _ := sid.String() 194 | if s == "" { 195 | return "unknown" 196 | } 197 | return s 198 | } 199 | 200 | func GroupID(info os.FileInfo) string { 201 | path := info.Name() 202 | if info.(*item.FileInfo).FullPath != "" { 203 | path = info.(*item.FileInfo).FullPath 204 | } 205 | var needed uint32 206 | fromString, err := syscall.UTF16PtrFromString(path) 207 | if err != nil { 208 | return "unknown" 209 | } 210 | _, _, _ = procGetFileSecurity.Call( 211 | uintptr(unsafe.Pointer(fromString)), 212 | 0x00000001, /* OWNER_SECURITY_INFORMATION */ 213 | 0, 214 | 0, 215 | uintptr(unsafe.Pointer(&needed)), 216 | ) 217 | 218 | if needed == 0 { 219 | return "unknown" 220 | } 221 | 222 | buf := make([]byte, needed) 223 | r1, _, err := procGetFileSecurity.Call( 224 | uintptr(unsafe.Pointer(fromString)), 225 | 0x00000001, /* OWNER_SECURITY_INFORMATION */ 226 | uintptr(unsafe.Pointer(&buf[0])), 227 | uintptr(needed), 228 | uintptr(unsafe.Pointer(&needed)), 229 | ) 230 | if r1 == 0 && err != nil { 231 | return "unknown" 232 | } 233 | var ownerDefaulted uint32 234 | var sid *syscall.SID 235 | r1, _, err = procGetSecurityDescriptorOwner.Call( 236 | uintptr(unsafe.Pointer(&buf[0])), 237 | uintptr(unsafe.Pointer(&sid)), 238 | uintptr(unsafe.Pointer(&ownerDefaulted)), 239 | ) 240 | if r1 == 0 && err != nil { 241 | return "unknown" 242 | } 243 | s, _ := sid.String() 244 | if s == "" { 245 | return "unknown" 246 | } 247 | return s 248 | } 249 | --------------------------------------------------------------------------------