feat: use filepicker bubble (#289)

* feat: use filepicker bubble

* fix: use new API

* chore: bump bubbles
This commit is contained in:
Maas Lalani 2023-03-06 11:54:02 -05:00 committed by GitHub
parent 99e6625a39
commit b5444d5f0b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 42 additions and 296 deletions

View file

@ -7,8 +7,8 @@ import (
"path/filepath"
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/filepicker"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stack"
"github.com/charmbracelet/gum/style"
)
@ -27,29 +27,26 @@ func (o Options) Run() error {
return fmt.Errorf("file not found: %w", err)
}
m := model{
path: path,
cursor: o.Cursor,
selected: 0,
showHidden: o.All,
dirAllowed: o.Directory,
fileAllowed: o.File,
autoHeight: o.Height == 0,
height: o.Height,
max: 0,
min: 0,
selectedStack: stack.NewStack(),
minStack: stack.NewStack(),
maxStack: stack.NewStack(),
cursorStyle: o.CursorStyle.ToLipgloss().Inline(true),
symlinkStyle: o.SymlinkStyle.ToLipgloss().Inline(true),
directoryStyle: o.DirectoryStyle.ToLipgloss().Inline(true),
fileStyle: o.FileStyle.ToLipgloss().Inline(true),
permissionStyle: o.PermissionsStyle.ToLipgloss().Inline(true),
selectedStyle: o.SelectedStyle.ToLipgloss().Inline(true),
fileSizeStyle: o.FileSizeStyle.ToLipgloss().Inline(true),
fp := filepicker.New()
fp.Path = path
fp.Height = o.Height
fp.AutoHeight = o.Height == 0
fp.Cursor = o.Cursor
fp.DirAllowed = o.Directory
fp.FileAllowed = o.File
fp.ShowHidden = o.All
fp.Styles = filepicker.Styles{
Cursor: o.CursorStyle.ToLipgloss(),
Symlink: o.SymlinkStyle.ToLipgloss(),
Directory: o.DirectoryStyle.ToLipgloss(),
File: o.FileStyle.ToLipgloss(),
Permission: o.PermissionsStyle.ToLipgloss(),
Selected: o.SelectedStyle.ToLipgloss(),
FileSize: o.FileSizeStyle.ToLipgloss(),
}
m := model{filepicker: fp}
tm, err := tea.NewProgram(&m, tea.WithOutput(os.Stderr)).Run()
if err != nil {
return fmt.Errorf("unable to pick selection: %w", err)
@ -57,11 +54,11 @@ func (o Options) Run() error {
m = tm.(model)
if m.path == "" {
if m.selectedPath == "" {
os.Exit(1)
}
fmt.Println(m.path)
fmt.Println(m.selectedPath)
return nil
}

View file

@ -13,268 +13,41 @@
package file
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"github.com/charmbracelet/bubbles/filepicker"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stack"
"github.com/charmbracelet/lipgloss"
"github.com/dustin/go-humanize"
)
const marginBottom = 5
type model struct {
quitting bool
path string
files []os.DirEntry
showHidden bool
dirAllowed bool
fileAllowed bool
selected int
selectedStack stack.Stack
min int
max int
maxStack stack.Stack
minStack stack.Stack
height int
autoHeight bool
cursor string
cursorStyle lipgloss.Style
symlinkStyle lipgloss.Style
directoryStyle lipgloss.Style
fileStyle lipgloss.Style
permissionStyle lipgloss.Style
selectedStyle lipgloss.Style
fileSizeStyle lipgloss.Style
}
type readDirMsg []os.DirEntry
func readDir(path string, showHidden bool) tea.Cmd {
return func() tea.Msg {
dirEntries, err := os.ReadDir(path)
if err != nil {
return tea.Quit
}
sort.Slice(dirEntries, func(i, j int) bool {
if dirEntries[i].IsDir() == dirEntries[j].IsDir() {
return dirEntries[i].Name() < dirEntries[j].Name()
}
return dirEntries[i].IsDir()
})
if showHidden {
return readDirMsg(dirEntries)
}
var sanitizedDirEntries []fs.DirEntry
for _, dirEntry := range dirEntries {
isHidden, _ := IsHidden(dirEntry.Name())
if isHidden {
continue
}
sanitizedDirEntries = append(sanitizedDirEntries, dirEntry)
}
return readDirMsg(sanitizedDirEntries)
}
filepicker filepicker.Model
selectedPath string
quitting bool
}
func (m model) Init() tea.Cmd {
return readDir(m.path, m.showHidden)
return m.filepicker.Init()
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case readDirMsg:
m.files = msg
case tea.WindowSizeMsg:
if m.autoHeight {
m.height = msg.Height - marginBottom
}
m.max = m.height
case tea.KeyMsg:
switch msg.String() {
case "g":
m.selected = 0
m.min = 0
m.max = m.height - 1
case "G":
m.selected = len(m.files) - 1
m.min = len(m.files) - m.height
m.max = len(m.files) - 1
case "j", "down":
m.selected++
if m.selected >= len(m.files) {
m.selected = len(m.files) - 1
}
if m.selected > m.max {
m.min++
m.max++
}
case "k", "up":
m.selected--
if m.selected < 0 {
m.selected = 0
}
if m.selected < m.min {
m.min--
m.max--
}
case "J", "pgdown":
m.selected += m.height
if m.selected >= len(m.files) {
m.selected = len(m.files) - 1
}
m.min += m.height
m.max += m.height
if m.max >= len(m.files) {
m.max = len(m.files) - 1
m.min = m.max - m.height
}
case "K", "pgup":
m.selected -= m.height
if m.selected < 0 {
m.selected = 0
}
m.min -= m.height
m.max -= m.height
if m.min < 0 {
m.min = 0
m.max = m.min + m.height
}
case "ctrl+c", "q":
m.path = ""
m.quitting = true
return m, tea.Quit
case "backspace", "h", "left":
m.path = filepath.Dir(m.path)
if m.selectedStack.Length() > 0 {
m.selected, m.min, m.max = m.popView()
} else {
m.selected = 0
m.min = 0
m.max = m.height - 1
}
return m, readDir(m.path, m.showHidden)
case "l", "right", "enter":
if len(m.files) == 0 {
break
}
f := m.files[m.selected]
info, err := f.Info()
if err != nil {
break
}
isSymlink := info.Mode()&fs.ModeSymlink != 0
isDir := f.IsDir()
if isSymlink {
symlinkPath, _ := filepath.EvalSymlinks(filepath.Join(m.path, f.Name()))
info, err := os.Stat(symlinkPath)
if err != nil {
break
}
if info.IsDir() {
isDir = true
}
}
if (!isDir && m.fileAllowed) || (isDir && m.dirAllowed) {
if msg.String() == "enter" {
m.path = filepath.Join(m.path, f.Name())
m.quitting = true
return m, tea.Quit
}
}
if !isDir {
break
}
m.path = filepath.Join(m.path, f.Name())
m.pushView()
m.selected = 0
m.min = 0
m.max = m.height - 1
return m, readDir(m.path, m.showHidden)
}
}
return m, nil
}
func (m model) pushView() {
m.minStack.Push(m.min)
m.maxStack.Push(m.max)
m.selectedStack.Push(m.selected)
}
func (m model) popView() (int, int, int) {
return m.selectedStack.Pop(), m.minStack.Pop(), m.maxStack.Pop()
var cmd tea.Cmd
m.filepicker, cmd = m.filepicker.Update(msg)
if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
m.selectedPath = path
m.quitting = true
return m, tea.Quit
}
return m, cmd
}
func (m model) View() string {
if m.quitting {
return ""
}
if len(m.files) == 0 {
return "Bummer. No files found."
}
var s strings.Builder
for i, f := range m.files {
if i < m.min {
continue
}
if i > m.max {
break
}
var symlinkPath string
info, _ := f.Info()
isSymlink := info.Mode()&fs.ModeSymlink != 0
size := humanize.Bytes(uint64(info.Size()))
name := f.Name()
if isSymlink {
symlinkPath, _ = filepath.EvalSymlinks(filepath.Join(m.path, name))
}
if m.selected == i {
selected := fmt.Sprintf(" %s %"+fmt.Sprint(m.fileSizeStyle.GetWidth())+"s %s", info.Mode().String(), size, name)
if isSymlink {
selected = fmt.Sprintf("%s → %s", selected, symlinkPath)
}
s.WriteString(m.cursorStyle.Render(m.cursor) + m.selectedStyle.Render(selected))
s.WriteRune('\n')
continue
}
var style = m.fileStyle
if f.IsDir() {
style = m.directoryStyle
} else if isSymlink {
style = m.symlinkStyle
}
fileName := style.Render(name)
if isSymlink {
fileName = fmt.Sprintf("%s → %s", fileName, symlinkPath)
}
s.WriteString(fmt.Sprintf(" %s %s %s", m.permissionStyle.Render(info.Mode().String()), m.fileSizeStyle.Render(size), fileName))
s.WriteRune('\n')
}
return s.String()
return m.filepicker.View()
}

View file

@ -1,10 +0,0 @@
//go:build !windows
package file
import "strings"
// IsHidden reports whether a file is hidden or not.
func IsHidden(file string) (bool, error) {
return strings.HasPrefix(file, "."), nil
}

View file

@ -1,20 +0,0 @@
//go:build windows
package file
import (
"syscall"
)
// IsHidden reports whether a file is hidden or not.
func IsHidden(file string) (bool, error) {
pointer, err := syscall.UTF16PtrFromString(file)
if err != nil {
return false, err
}
attributes, err := syscall.GetFileAttributes(pointer)
if err != nil {
return false, err
}
return attributes&syscall.FILE_ATTRIBUTE_HIDDEN != 0, nil
}

2
go.mod
View file

@ -5,7 +5,7 @@ go 1.18
require (
github.com/alecthomas/kong v0.7.1
github.com/alecthomas/mango-kong v0.1.0
github.com/charmbracelet/bubbles v0.15.0
github.com/charmbracelet/bubbles v0.15.1-0.20230306155959-3372cf1aea2b
github.com/charmbracelet/bubbletea v0.23.2
github.com/charmbracelet/glamour v0.6.0
github.com/charmbracelet/lipgloss v0.6.1-0.20230222162833-a74950e6da16

6
go.sum
View file

@ -23,6 +23,12 @@ github.com/charmbracelet/bubbles v0.14.1-0.20221006154229-d1775121146a h1:/prXWl
github.com/charmbracelet/bubbles v0.14.1-0.20221006154229-d1775121146a/go.mod h1:5rZgJTHmgWISQnxnzzIJtQt3GC1bfJfNmr4SEtRDtTQ=
github.com/charmbracelet/bubbles v0.15.0 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI=
github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74=
github.com/charmbracelet/bubbles v0.15.1-0.20230303175231-2694acc38055 h1:4zkjfTm5xydSP+cZjgWyAmrjJXlx0E0E2DWu4x3r0vM=
github.com/charmbracelet/bubbles v0.15.1-0.20230303175231-2694acc38055/go.mod h1:2ZpQTAUWMFMWM5lx7FvucnwMOE+/JKP9d/Su1MKpZbo=
github.com/charmbracelet/bubbles v0.15.1-0.20230303214459-958a0ea710f1 h1:BcxSgchb4ZV1lJe9W9+LhQj6S3JouC39yYswVI4YOrw=
github.com/charmbracelet/bubbles v0.15.1-0.20230303214459-958a0ea710f1/go.mod h1:39HL8bnL0foloiENA/KvD+3mNg5SqWQV2Qh3eY/4ey4=
github.com/charmbracelet/bubbles v0.15.1-0.20230306155959-3372cf1aea2b h1:K9dWJ2spDhDhIrqnchjG867djPxWWe3mwdk6RdLMfhg=
github.com/charmbracelet/bubbles v0.15.1-0.20230306155959-3372cf1aea2b/go.mod h1:39HL8bnL0foloiENA/KvD+3mNg5SqWQV2Qh3eY/4ey4=
github.com/charmbracelet/bubbletea v0.22.1/go.mod h1:8/7hVvbPN6ZZPkczLiB8YpLkLJ0n7DMho5Wvfd2X1C0=
github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
github.com/charmbracelet/bubbletea v0.23.2 h1:vuUJ9HJ7b/COy4I30e8xDVQ+VRDUEFykIjryPfgsdps=