| CARVIEW |
Select Language
HTTP/2 200
date: Sat, 27 Dec 2025 21:30:42 GMT
content-type: text/html; charset=utf-8
vary: X-PJAX, X-PJAX-Container, Turbo-Visit, Turbo-Frame, X-Requested-With,Accept-Encoding, Accept, X-Requested-With
etag: W/"858b46df8747b5249cb728f3cd29cdd4"
cache-control: max-age=0, private, must-revalidate
strict-transport-security: max-age=31536000; includeSubdomains; preload
x-frame-options: deny
x-content-type-options: nosniff
x-xss-protection: 0
referrer-policy: no-referrer-when-downgrade
content-security-policy: default-src 'none'; base-uri 'self'; child-src github.githubassets.com github.com/assets-cdn/worker/ github.com/assets/ gist.github.com/assets-cdn/worker/; connect-src 'self' uploads.github.com www.githubstatus.com collector.github.com raw.githubusercontent.com api.github.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com *.rel.tunnels.api.visualstudio.com wss://*.rel.tunnels.api.visualstudio.com github.githubassets.com objects-origin.githubusercontent.com copilot-proxy.githubusercontent.com proxy.individual.githubcopilot.com proxy.business.githubcopilot.com proxy.enterprise.githubcopilot.com *.actions.githubusercontent.com wss://*.actions.githubusercontent.com productionresultssa0.blob.core.windows.net/ productionresultssa1.blob.core.windows.net/ productionresultssa2.blob.core.windows.net/ productionresultssa3.blob.core.windows.net/ productionresultssa4.blob.core.windows.net/ productionresultssa5.blob.core.windows.net/ productionresultssa6.blob.core.windows.net/ productionresultssa7.blob.core.windows.net/ productionresultssa8.blob.core.windows.net/ productionresultssa9.blob.core.windows.net/ productionresultssa10.blob.core.windows.net/ productionresultssa11.blob.core.windows.net/ productionresultssa12.blob.core.windows.net/ productionresultssa13.blob.core.windows.net/ productionresultssa14.blob.core.windows.net/ productionresultssa15.blob.core.windows.net/ productionresultssa16.blob.core.windows.net/ productionresultssa17.blob.core.windows.net/ productionresultssa18.blob.core.windows.net/ productionresultssa19.blob.core.windows.net/ github-production-repository-image-32fea6.s3.amazonaws.com github-production-release-asset-2e65be.s3.amazonaws.com insights.github.com wss://alive.github.com wss://alive-staging.github.com api.githubcopilot.com api.individual.githubcopilot.com api.business.githubcopilot.com api.enterprise.githubcopilot.com; font-src github.githubassets.com; form-action 'self' github.com gist.github.com copilot-workspace.githubnext.com objects-origin.githubusercontent.com; frame-ancestors 'none'; frame-src viewscreen.githubusercontent.com notebooks.githubusercontent.com; img-src 'self' data: blob: github.githubassets.com media.githubusercontent.com camo.githubusercontent.com identicons.github.com avatars.githubusercontent.com private-avatars.githubusercontent.com github-cloud.s3.amazonaws.com objects.githubusercontent.com release-assets.githubusercontent.com secured-user-images.githubusercontent.com/ user-images.githubusercontent.com/ private-user-images.githubusercontent.com opengraph.githubassets.com marketplace-screenshots.githubusercontent.com/ copilotprodattachments.blob.core.windows.net/github-production-copilot-attachments/ github-production-user-asset-6210df.s3.amazonaws.com customer-stories-feed.github.com spotlights-feed.github.com objects-origin.githubusercontent.com *.githubusercontent.com; manifest-src 'self'; media-src github.com user-images.githubusercontent.com/ secured-user-images.githubusercontent.com/ private-user-images.githubusercontent.com github-production-user-asset-6210df.s3.amazonaws.com gist.github.com github.githubassets.com; script-src github.githubassets.com; style-src 'unsafe-inline' github.githubassets.com; upgrade-insecure-requests; worker-src github.githubassets.com github.com/assets-cdn/worker/ github.com/assets/ gist.github.com/assets-cdn/worker/
server: github.com
content-encoding: gzip
accept-ranges: bytes
set-cookie: _gh_sess=us%2Fujef765XX1fdMXQbtk3TjLNIFPlrubGG4S1hRrh4lc4MwutyVx5Ji7lL6vx0IxxrqArkNri8zj6TKYJ8gVgpR%2BTcTpro3jWXHTWrEfqRTWxVnezh2fJdJj7K1ekO1Dz9ImAmXytCL%2F9AFzrVkoQIRYXuOwC5IpPa5DSaJmpttKJ%2Bi3nlrjqXD%2Bg9oqyzWYHqOnk0gMQ%2F6XYhLmSS2JZgKK26XdMOqI3DtD%2BOObsd8n%2FVUcb%2F9omHvXfZbKjQqIHXmGe7s%2F0i2GNmFCIAzLA%3D%3D--AoZgNDqLTyCTlTYr--En8clwPlyUTBC%2BOF0h3D8Q%3D%3D; Path=/; HttpOnly; Secure; SameSite=Lax
set-cookie: _octo=GH1.1.244577252.1766871041; Path=/; Domain=github.com; Expires=Sun, 27 Dec 2026 21:30:41 GMT; Secure; SameSite=Lax
set-cookie: logged_in=no; Path=/; Domain=github.com; Expires=Sun, 27 Dec 2026 21:30:41 GMT; HttpOnly; Secure; SameSite=Lax
x-github-request-id: 8FCE:382597:48CF504:57C07F4:69505001
GitHub - antonmedv/ll: Opinionated ls rewrite in Go 🧦
Skip to content
Navigation Menu
{{ message }}
-
Notifications
You must be signed in to change notification settings - Fork 2
antonmedv/ll
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
// .-. .-.
// | | | |
// | `--.| `--.
// `----'`----'
//
// ll – a small utility to list files in the current directory.
//
// # Why?
// Because I wanted to display files in columns with git status.
//
// # Rationalize
// One entry per line for lots of files can't be fitted on a screen
// and requires scrolling. With the multi-column layout, space can be
// used more efficiently. At the same time, git status information is
// also often needed.
package main
import (
"bytes"
"fmt"
"io/ioutil"
"math"
"os"
"os/exec"
"path/filepath"
. "strings"
"sync"
"time"
"golang.org/x/sys/unix"
)
const (
modified = "\033[0;34m%s\033[0m"
added = "\033[0;32m%s\033[0m"
untracked = "\033[0;31m%s\033[0m"
bold = "\033[1m%v\033[0m"
)
var (
spinner = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
sizes = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
base = float64(1000)
)
func main() {
if len(os.Args) == 2 {
ll(os.Args[1])
return
}
if len(os.Args) > 2 {
for i := 1; i < len(os.Args); i++ {
path, _ := filepath.Abs(os.Args[i])
printInfo(fileInfo(path), path)
}
return
}
pwd, err := os.Getwd()
if err != nil {
panic(err)
}
ll(pwd)
}
func ll(cwd string) {
// Maybe it is and argument, so get absolute path.
cwd, _ = filepath.Abs(cwd)
// Is it a file?
if fi := fileInfo(cwd); !fi.IsDir() {
printInfo(fi, cwd)
return
}
// ReadDir already returns files and dirs sorted by filename.
files, err := ioutil.ReadDir(cwd)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if len(files) == 0 {
return
}
// We need terminal size to nicely fit on screen.
var width, height int
ws, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ)
if err != nil || ws == nil {
width, height = 80, 60
} else {
width, height = int(ws.Col), int(ws.Row)
}
// If it's possible to fit all files in one column on half of screen, just use one column.
// Otherwise let's squeeze listing in half of screen.
columns := len(files)/(height/2) + 1
// Gonna keep file names and format string for git status.
modes := map[string]string{}
// If stdout of ll piped, use ls behavior: one line, no colors.
fi, err := os.Stdout.Stat()
if err != nil {
panic(err)
}
if (fi.Mode() & os.ModeCharDevice) == 0 {
columns = 1
} else {
status := gitStatus()
for _, file := range files {
name := file.Name()
if file.IsDir() {
name += "/"
}
// gitStatus returns file names of modified files from repo root.
fullPath := filepath.Join(cwd, name)
for path, mode := range status {
if subPath(path, fullPath) {
if mode[0] == '?' || mode[1] == '?' {
modes[name] = untracked
} else if mode[0] == 'A' || mode[1] == 'A' {
modes[name] = added
} else if mode[0] == 'M' || mode[1] == 'M' {
modes[name] = modified
}
}
}
}
}
start:
// Let's try to fit everything in terminal width with this many columns.
// If we are not able to do it, decrease column number and goto start.
rows := int(math.Ceil(float64(len(files)) / float64(columns)))
names := make([][]string, columns)
n := 0
for i := 0; i < columns; i++ {
names[i] = make([]string, rows)
// Columns size is going to be of max file name size.
max := 0
for j := 0; j < rows; j++ {
name := ""
if n < len(files) {
name = files[n].Name()
if files[n].IsDir() {
// Dir should have slash at end.
name += "/"
}
n++
}
if max < len(name) {
max = len(name)
}
names[i][j] = name
}
// Append spaces to make all names in one column of same size.
for j := 0; j < rows; j++ {
names[i][j] += Repeat(" ", max-len(names[i][j]))
}
}
const separator = " " // Separator between columns.
for j := 0; j < rows; j++ {
row := make([]string, columns)
for i := 0; i < columns; i++ {
row[i] = names[i][j]
}
if len(Join(row, separator)) > width && columns > 1 {
// Yep. No luck, let's decrease number of columns and try one more time.
columns--
goto start
}
}
// Let's add colors from git status to file names.
output := make([]string, rows)
for j := 0; j < rows; j++ {
row := make([]string, columns)
for i := 0; i < columns; i++ {
f, ok := modes[TrimRight(names[i][j], " ")]
if !ok {
f = "%s"
}
row[i] = fmt.Sprintf(f, names[i][j])
}
output[j] = Join(row, separator)
}
fmt.Println(Join(output, "\n"))
}
func subPath(path string, fullPath string) bool {
p := Split(path, "/")
for i, s := range Split(fullPath, "/") {
if i >= len(p) {
return false
}
if p[i] != s {
return false
}
}
return true
}
func gitRepo() (string, error) {
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
return Trim(out.String(), "\n"), err
}
func gitStatus() map[string]string {
repo, err := gitRepo()
if err != nil {
return nil
}
cmd := exec.Command("git", "status", "--porcelain=v1")
var out bytes.Buffer
cmd.Stdout = &out
err = cmd.Run()
if err != nil {
return nil
}
m := map[string]string{}
for _, line := range Split(Trim(out.String(), "\n"), "\n") {
if len(line) == 0 {
continue
}
m[filepath.Join(repo, line[3:])] = line[:2]
}
return m
}
func printInfo(fi os.FileInfo, path string) {
name := fi.Name()
size := fi.Size()
if fi.IsDir() {
name += "/"
done := make(chan bool)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
i, t := 0, time.Tick(100*time.Millisecond)
for {
select {
case <-t:
fmt.Printf("\r%v\t%v", spinner[i%len(spinner)], name)
i++
case <-done:
fmt.Print("\r")
return
}
}
}()
size, _ = dirSize(path)
done <- true
wg.Wait()
}
fmt.Printf("%v\t%v\n", toHuman(size), name)
}
func fileInfo(path string) os.FileInfo {
fi, err := os.Stat(path)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
return fi
}
func toHuman(s int64) string {
if s < 10 {
value := fmt.Sprintf(bold, s)
return fmt.Sprintf(" %v B", value)
}
e := math.Floor(math.Log(float64(s)) / math.Log(base))
suffix := sizes[int(e)]
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
f := "%3.0f"
if val < 10 {
f = "%3.1f"
}
value := fmt.Sprintf(bold, fmt.Sprintf(f, val))
return fmt.Sprintf("%v %v", value, suffix)
}
func dirSize(path string) (int64, error) {
var size int64
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
size += info.Size()
}
return err
})
return size, err
}
About
Opinionated ls rewrite in Go 🧦
Resources
Stars
Watchers
Forks
You can’t perform that action at this time.