diff --git a/file/command.go b/file/command.go index f6ece8f..8bedc85 100644 --- a/file/command.go +++ b/file/command.go @@ -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 } diff --git a/file/file.go b/file/file.go index 22fa7ec..b0147e0 100644 --- a/file/file.go +++ b/file/file.go @@ -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() } diff --git a/file/hidden_unix.go b/file/hidden_unix.go deleted file mode 100644 index a3e2cb1..0000000 --- a/file/hidden_unix.go +++ /dev/null @@ -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 -} diff --git a/file/hidden_windows.go b/file/hidden_windows.go deleted file mode 100644 index f44fdfc..0000000 --- a/file/hidden_windows.go +++ /dev/null @@ -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 -} diff --git a/go.mod b/go.mod index d9772c8..4d73668 100644 --- a/go.mod +++ b/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.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 diff --git a/go.sum b/go.sum index cadc19b..8634861 100644 --- a/go.sum +++ b/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.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=