mirror of
https://github.com/charmbracelet/gum
synced 2024-06-27 01:30:06 +02:00
feat: use filepicker bubble
This commit is contained in:
parent
99e6625a39
commit
f1e04d11ec
|
@ -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
|
||||
}
|
||||
|
|
254
file/file.go
254
file/file.go
|
@ -13,268 +13,40 @@
|
|||
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 filepicker.FileSelectedMsg:
|
||||
m.selectedPath = msg.Path
|
||||
m.quitting = true
|
||||
return m, tea.Quit
|
||||
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)
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
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.20230303175231-2694acc38055
|
||||
github.com/charmbracelet/bubbletea v0.23.2
|
||||
github.com/charmbracelet/glamour v0.6.0
|
||||
github.com/charmbracelet/lipgloss v0.6.1-0.20230222162833-a74950e6da16
|
||||
|
|
2
go.sum
2
go.sum
|
@ -23,6 +23,8 @@ 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/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=
|
||||
|
|
Loading…
Reference in a new issue