mirror of
https://github.com/charmbracelet/gum
synced 2024-06-16 12:35:06 +02:00
feat: use filepicker bubble (#289)
* feat: use filepicker bubble * fix: use new API * chore: bump bubbles
This commit is contained in:
parent
99e6625a39
commit
b5444d5f0b
|
@ -7,8 +7,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
|
"github.com/charmbracelet/bubbles/filepicker"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/gum/internal/stack"
|
|
||||||
"github.com/charmbracelet/gum/style"
|
"github.com/charmbracelet/gum/style"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,29 +27,26 @@ func (o Options) Run() error {
|
||||||
return fmt.Errorf("file not found: %w", err)
|
return fmt.Errorf("file not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m := model{
|
fp := filepicker.New()
|
||||||
path: path,
|
fp.Path = path
|
||||||
cursor: o.Cursor,
|
fp.Height = o.Height
|
||||||
selected: 0,
|
fp.AutoHeight = o.Height == 0
|
||||||
showHidden: o.All,
|
fp.Cursor = o.Cursor
|
||||||
dirAllowed: o.Directory,
|
fp.DirAllowed = o.Directory
|
||||||
fileAllowed: o.File,
|
fp.FileAllowed = o.File
|
||||||
autoHeight: o.Height == 0,
|
fp.ShowHidden = o.All
|
||||||
height: o.Height,
|
fp.Styles = filepicker.Styles{
|
||||||
max: 0,
|
Cursor: o.CursorStyle.ToLipgloss(),
|
||||||
min: 0,
|
Symlink: o.SymlinkStyle.ToLipgloss(),
|
||||||
selectedStack: stack.NewStack(),
|
Directory: o.DirectoryStyle.ToLipgloss(),
|
||||||
minStack: stack.NewStack(),
|
File: o.FileStyle.ToLipgloss(),
|
||||||
maxStack: stack.NewStack(),
|
Permission: o.PermissionsStyle.ToLipgloss(),
|
||||||
cursorStyle: o.CursorStyle.ToLipgloss().Inline(true),
|
Selected: o.SelectedStyle.ToLipgloss(),
|
||||||
symlinkStyle: o.SymlinkStyle.ToLipgloss().Inline(true),
|
FileSize: o.FileSizeStyle.ToLipgloss(),
|
||||||
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),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m := model{filepicker: fp}
|
||||||
|
|
||||||
tm, err := tea.NewProgram(&m, tea.WithOutput(os.Stderr)).Run()
|
tm, err := tea.NewProgram(&m, tea.WithOutput(os.Stderr)).Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to pick selection: %w", err)
|
return fmt.Errorf("unable to pick selection: %w", err)
|
||||||
|
@ -57,11 +54,11 @@ func (o Options) Run() error {
|
||||||
|
|
||||||
m = tm.(model)
|
m = tm.(model)
|
||||||
|
|
||||||
if m.path == "" {
|
if m.selectedPath == "" {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(m.path)
|
fmt.Println(m.selectedPath)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
255
file/file.go
255
file/file.go
|
@ -13,268 +13,41 @@
|
||||||
package file
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"github.com/charmbracelet/bubbles/filepicker"
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
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 {
|
type model struct {
|
||||||
quitting bool
|
filepicker filepicker.Model
|
||||||
path string
|
selectedPath string
|
||||||
files []os.DirEntry
|
quitting bool
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) Init() tea.Cmd {
|
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) {
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
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:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
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":
|
case "ctrl+c", "q":
|
||||||
m.path = ""
|
|
||||||
m.quitting = true
|
|
||||||
return m, tea.Quit
|
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
|
var cmd tea.Cmd
|
||||||
}
|
m.filepicker, cmd = m.filepicker.Update(msg)
|
||||||
|
if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
|
||||||
func (m model) pushView() {
|
m.selectedPath = path
|
||||||
m.minStack.Push(m.min)
|
m.quitting = true
|
||||||
m.maxStack.Push(m.max)
|
return m, tea.Quit
|
||||||
m.selectedStack.Push(m.selected)
|
}
|
||||||
}
|
return m, cmd
|
||||||
|
|
||||||
func (m model) popView() (int, int, int) {
|
|
||||||
return m.selectedStack.Pop(), m.minStack.Pop(), m.maxStack.Pop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) View() string {
|
func (m model) View() string {
|
||||||
if m.quitting {
|
if m.quitting {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if len(m.files) == 0 {
|
return m.filepicker.View()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
2
go.mod
|
@ -5,7 +5,7 @@ go 1.18
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/kong v0.7.1
|
github.com/alecthomas/kong v0.7.1
|
||||||
github.com/alecthomas/mango-kong v0.1.0
|
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/bubbletea v0.23.2
|
||||||
github.com/charmbracelet/glamour v0.6.0
|
github.com/charmbracelet/glamour v0.6.0
|
||||||
github.com/charmbracelet/lipgloss v0.6.1-0.20230222162833-a74950e6da16
|
github.com/charmbracelet/lipgloss v0.6.1-0.20230222162833-a74950e6da16
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -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.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 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI=
|
||||||
github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74=
|
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.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.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
|
||||||
github.com/charmbracelet/bubbletea v0.23.2 h1:vuUJ9HJ7b/COy4I30e8xDVQ+VRDUEFykIjryPfgsdps=
|
github.com/charmbracelet/bubbletea v0.23.2 h1:vuUJ9HJ7b/COy4I30e8xDVQ+VRDUEFykIjryPfgsdps=
|
||||||
|
|
Loading…
Reference in a new issue