Skip to content

Commit

Permalink
Add status bar
Browse files Browse the repository at this point in the history
  • Loading branch information
antonmedv committed Feb 9, 2025
1 parent a927675 commit 0a960fa
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 8 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v0.26.5
github.com/charmbracelet/lipgloss v0.11.0
github.com/expr-lang/expr v1.16.9
github.com/mattn/go-runewidth v0.0.15
github.com/muesli/termenv v0.15.2
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelh
github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
Expand Down
4 changes: 2 additions & 2 deletions icons.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ func parseIcons() {
}

//go:embed etc/icons
var f embed.FS
var iconsFs embed.FS

func (im iconMap) parse() {
icons, _ := f.Open("etc/icons")
icons, _ := iconsFs.Open("etc/icons")
pairs, err := readPairs(icons)
if err != nil {
log.Printf("reading icons file: %s", err)
Expand Down
44 changes: 38 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
"github.com/mattn/go-runewidth"
"github.com/muesli/termenv"
"github.com/sahilm/fuzzy"
Expand Down Expand Up @@ -58,6 +60,10 @@ func main() {
positions: make(map[string]position),
}

if statusBar, ok := os.LookupEnv("WALK_STATUS_BAR"); ok {
m.statusBar = compile(statusBar)
}

argsWithoutFlags := make([]string, 0)
for i := 1; i < len(os.Args); i++ {
if os.Args[i] == "--help" || os.Args[1] == "-h" {
Expand Down Expand Up @@ -146,6 +152,7 @@ type model struct {
yankedFilePath string // Show yank info
hideHidden bool // Hide hidden files
showHelp bool // Show help
statusBar *vm.Program // Status bar program.
}

type position struct {
Expand Down Expand Up @@ -178,7 +185,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Reset position history as c&r changes.
m.positions = make(map[string]position)
// Keep cursor at same place.
fileName, ok := m.fileName()
fileName, ok := m.currentFileName()
if ok {
m.prevName = fileName
m.findPrevName = true
Expand Down Expand Up @@ -336,7 +343,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Reset position history as c&r changes.
m.positions = make(map[string]position)
// Keep cursor at same place.
fileName, ok := m.fileName()
fileName, ok := m.currentFileName()
if !ok {
return m, nil
}
Expand Down Expand Up @@ -510,7 +517,7 @@ func (m *model) View() string {
}

// Preview pane.
fileName, _ := m.fileName()
fileName, _ := m.currentFileName()
previewPane := bar.Render(fileName) + "\n"
previewPane += m.previewContent

Expand Down Expand Up @@ -558,6 +565,20 @@ func (m *model) View() string {
} else if m.yankedFilePath != "" {
yankBar := fmt.Sprintf("copied: %v", m.yankedFilePath)
main += "\n" + bar.Render(yankBar)
} else if m.statusBar != nil {
f, ok := m.currentFile()
if ok {
env := Env{
Files: m.files,
CurrentFile: f,
}
statusBar, err := expr.Run(m.statusBar, env)
if err != nil {
main += "\n" + err.Error()
} else {
main += "\n" + bar.Render(fmt.Sprintf("%v", statusBar))
}
}
}
}

Expand Down Expand Up @@ -704,6 +725,9 @@ func (m *model) showStatusBar() bool {
if m.yankedFilePath != "" {
return true
}
if m.statusBar != nil {
return true
}
return false
}

Expand Down Expand Up @@ -734,16 +758,24 @@ func (m *model) saveCursorPosition() {
}
}

func (m *model) fileName() (string, bool) {
func (m *model) currentFile() (fs.DirEntry, bool) {
i := m.c*m.rows + m.r
if i >= len(m.files) || i < 0 {
return nil, false
}
return m.files[i], true
}

func (m *model) currentFileName() (string, bool) {
f, ok := m.currentFile()
if !ok {
return "", false
}
return m.files[i].Name(), true
return f.Name(), true
}

func (m *model) filePath() (string, bool) {
fileName, ok := m.fileName()
fileName, ok := m.currentFileName()
if !ok {
return fileName, false
}
Expand Down
158 changes: 158 additions & 0 deletions status_bar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package main

import (
"errors"
"fmt"
"io/fs"
"math"
"os/user"
"strconv"
"syscall"
"time"

"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
)

func compile(code string) *vm.Program {
p, err := expr.Compile(code, expr.Env(Env{}))
if err != nil {
panic(err)
}
return p
}

type Env struct {
Files []fs.DirEntry `expr:"files"`
CurrentFile fs.DirEntry `expr:"current_file"`
}

func (e Env) Sprintf(format string, a ...any) string {
return fmt.Sprintf(format, a...)
}

func (e Env) PadLeft(s string, n int) string {
return fmt.Sprintf("%*s", n, s)
}

func (e Env) PadRight(s string, n int) string {
return fmt.Sprintf("%*s", n, s)
}

func (e Env) Size() string {
info, err := e.CurrentFile.Info()
if err != nil {
return "N/A"
}
size := float64(info.Size())
if size == 0 {
return "0B"
}
units := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"}
base := math.Log(size) / math.Log(1024)
unitIndex := int(math.Floor(base))
if unitIndex >= len(units) {
unitIndex = len(units) - 1
}
value := size / math.Pow(1024, float64(unitIndex))
if unitIndex == 0 {
return fmt.Sprintf("%.0f%s", value, units[unitIndex])
}
return fmt.Sprintf("%.1f%s", value, units[unitIndex])
}

func (e Env) Mode() string {
info, err := e.CurrentFile.Info()
if err != nil {
return "?????????"
}

mode := info.Mode()

result := make([]byte, 10)

switch {
case mode.IsDir():
result[0] = 'd'
case mode&fs.ModeSymlink != 0:
result[0] = 'l'
case mode&fs.ModeSocket != 0:
result[0] = 's'
case mode&fs.ModeNamedPipe != 0:
result[0] = 'p'
case mode&fs.ModeCharDevice != 0:
result[0] = 'c'
case mode&fs.ModeDevice != 0:
result[0] = 'b'
default:
result[0] = '-'
}

// Owner permissions
result[1] = permBit(mode&0400, 'r')
result[2] = permBit(mode&0200, 'w')
result[3] = permBit(mode&0100, 'x')

// Group permissions
result[4] = permBit(mode&040, 'r')
result[5] = permBit(mode&020, 'w')
result[6] = permBit(mode&010, 'x')

// Others permissions
result[7] = permBit(mode&04, 'r')
result[8] = permBit(mode&02, 'w')
result[9] = permBit(mode&01, 'x')

// Handle special permission bits
if mode&fs.ModeSetuid != 0 {
result[3] = 's'
}
if mode&fs.ModeSetgid != 0 {
result[6] = 's'
}
if mode&fs.ModeSticky != 0 {
result[9] = 't'
}

return string(result)
}

func (e Env) ModTime() string {
info, err := e.CurrentFile.Info()
if err != nil {
return "???"
}
modTime := info.ModTime()
now := time.Now()
if modTime.Year() == now.Year() {
return modTime.Format("Jan 2 15:04")
}
return modTime.Format("Jan 2 2006")
}

func (e Env) Owner() (string, error) {
fileInfo, err := e.CurrentFile.Info()
if err != nil {
return "", err
}

stat, ok := fileInfo.Sys().(*syscall.Stat_t)
if !ok {
return "", errors.New("unsupported platform")
}

uidStr := strconv.FormatUint(uint64(stat.Uid), 10)
gidStr := strconv.FormatUint(uint64(stat.Gid), 10)

username := uidStr
if userInfo, err := user.LookupId(uidStr); err == nil {
username = userInfo.Username
}

groupname := gidStr
if groupInfo, err := user.LookupGroupId(gidStr); err == nil {
groupname = groupInfo.Name
}

return fmt.Sprintf("%s %s", username, groupname), nil
}
80 changes: 80 additions & 0 deletions status_bar_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package main

import (
"io/fs"
"testing"
)

type mockFile struct {
fs.DirEntry
mode fs.FileMode
size int64
}

type mockFileInfo struct {
fs.FileInfo
mode fs.FileMode
size int64
}

func (m mockFile) Info() (fs.FileInfo, error) {
return mockFileInfo{
size: m.size,
mode: m.mode,
}, nil
}

func (m mockFileInfo) Mode() fs.FileMode { return m.mode }

func (m mockFileInfo) Size() int64 { return m.size }

func TestFileSize(t *testing.T) {
testCases := []struct {
size int64
expected string
}{
{0, "0B"},
{500, "500B"},
{1024, "1.0KB"},
{1500, "1.5KB"},
{1024 * 1024, "1.0MB"},
{1024 * 1024 * 1024, "1.0GB"},
{1024 * 1024 * 1024 * 1024, "1.0TB"},
}

env := Env{}
for _, tc := range testCases {
mockFile := mockFile{size: tc.size}
result := env.FileSize(mockFile)
if result != tc.expected {
t.Errorf("Failed: %v != %v", result, tc.expected)
}
}
}

func TestFileMode(t *testing.T) {
testCases := []struct {
mode fs.FileMode
expected string
}{
{0755 | fs.ModeDir, "drwxr-xr-x"}, // Directory
{0644, "-rw-r--r--"}, // Regular file
{0777 | fs.ModeSetuid, "-rwsrwxrwx"}, // SetUID
{0777 | fs.ModeSetgid, "-rwxrwsrwx"}, // SetGID
{0777 | fs.ModeSticky, "-rwxrwxrwt"}, // Sticky bit
{fs.ModeSymlink | 0777, "lrwxrwxrwx"}, // Symbolic link
{fs.ModeSocket | 0777, "srwxrwxrwx"}, // Socket
{fs.ModeNamedPipe | 0777, "prwxrwxrwx"}, // Named pipe
{fs.ModeDevice | fs.ModeCharDevice | 0777, "crwxrwxrwx"}, // Character device
{fs.ModeDevice | 0777, "brwxrwxrwx"}, // Block device
}

env := Env{}
for _, tc := range testCases {
mockFile := mockFile{mode: tc.mode}
result := env.FileMode(mockFile)
if result != tc.expected {
t.Errorf("Failed: %v != %v", result, tc.expected)
}
}
}
Loading

0 comments on commit 0a960fa

Please sign in to comment.