├── 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 |
--------------------------------------------------------------------------------