Documentation
¶
Overview ¶
Package finder provides a cross-platform, terminal-based file and folder picker that works consistently across Windows, macOS, Linux, BSD, WSL, and Git Bash without any OS-specific dependencies.
The picker is built on the Bubble Tea TUI framework and uses no cgo, so it cross-compiles cleanly and runs in any environment where a terminal is available — including SSH sessions and Git Bash, where native OS file dialogs are not usable.
Quick Start ¶
The four Pick functions are the most common entry points:
path, err := finder.PickFile(finder.WithStartDir("~/projects"))
dir, err := finder.PickFolder()
any, err := finder.PickAny()
paths, err := finder.PickMultiple(finder.WithFilter("*.go"))
Each returns ErrCancelled if the user exits the picker without making a selection. Configuration is applied via functional Option values.
Modes ¶
The Mode constants control selection behavior:
- ModeFile — select a single file
- ModeFolder — select a single folder
- ModeAny — select either a file or folder
- ModeMultiple — select multiple files and/or folders across directories
Customization ¶
Every keybinding and visual style is overridable through WithKeyMap and WithStyles. See DefaultKeyMap and DefaultStyles for the defaults.
Interactive Mode ¶
Passing WithInteractive enables in-picker file management: press n to create a file, N to create a folder, and d to delete an entry. Typing a new file name ending in "/" creates a directory instead.
Utilities ¶
The package also exposes path utilities that are useful outside the picker itself, including ToWindowsPath, ToWSLPath, IsWSL, ResolvePath, NormalizePath, ParentDir, and FormatSize.
Index ¶
- Variables
- func CreateDir(dir, name string) error
- func CreateFile(dir, name string) error
- func DeletePath(p string) error
- func FormatSize(bytes int64) string
- func IsWSL() bool
- func NormalizePath(p string) string
- func ParentDir(dir string) string
- func PickAny(opts ...Option) (string, error)
- func PickFile(opts ...Option) (string, error)
- func PickFolder(opts ...Option) (string, error)
- func PickMultiple(opts ...Option) ([]string, error)
- func ResolvePath(p string) string
- func ToWSLPath(p string) string
- func ToWindowsPath(p string) string
- type DoneMsg
- type FileEntry
- type FileSystem
- type KeyMap
- type Mode
- type Model
- type Option
- func WithEmbedded(enabled bool) Option
- func WithExpandSymlinks(enabled bool) Option
- func WithExtensions(exts ...string) Option
- func WithFS(fsys fs.FS) Option
- func WithFilter(patterns ...string) Option
- func WithFuzzySearch(enabled bool) Option
- func WithHeight(h int) Option
- func WithHidden(show bool) Option
- func WithInteractive(enabled bool) Option
- func WithKeyMap(km KeyMap) Option
- func WithMode(m Mode) Option
- func WithPreview(enabled bool) Option
- func WithPreviewFunc(fn PreviewFunc) Option
- func WithStartDir(dir string) Option
- func WithStyles(s Styles) Option
- func WithTitle(title string) Option
- type Options
- type PreviewFunc
- type State
- type Styles
- type WritableFileSystem
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrCancelled = errors.New("picker cancelled")
ErrCancelled is returned when the user cancels the picker without selecting.
Functions ¶
func CreateDir ¶
CreateDir creates a new directory at the given path. Returns an error if the directory already exists, the name is invalid, or it cannot be created.
func CreateFile ¶
CreateFile creates an empty file at the given path. Returns an error if the file already exists, the name is invalid, or it cannot be created.
func DeletePath ¶
DeletePath removes a file or directory (and all contents) at the given path. Refuses to delete filesystem roots or paths that resolve to /.
func FormatSize ¶
FormatSize returns a human-readable file size string.
Example ¶
package main
import (
"fmt"
finder "github.com/rummage-dev/rummage"
)
func main() {
fmt.Println(finder.FormatSize(0))
fmt.Println(finder.FormatSize(1536))
fmt.Println(finder.FormatSize(1048576))
fmt.Println(finder.FormatSize(1073741824))
}
Output: 0 B 1.5 KB 1 MB 1 GB
func NormalizePath ¶
NormalizePath converts a path to the OS-native format and resolves it to an absolute path.
func ParentDir ¶
ParentDir returns the parent directory of the given path.
Example ¶
ParentDir returns the parent of a path, or the path itself if it has no parent (e.g. the filesystem root). The output uses the OS-native path separator, so on Windows /home/user becomes \home — for portable usage, pass paths through filepath.FromSlash first.
package main
import (
"fmt"
finder "github.com/rummage-dev/rummage"
)
func main() {
parent := finder.ParentDir("/home/user/projects")
fmt.Println(parent)
}
Output:
func PickAny ¶
PickAny opens an interactive picker that allows selecting either a file or a folder. Press enter to select the highlighted item, right/l to navigate into directories, or s to select the current directory. Returns ErrCancelled if the user exits without selecting.
func PickFile ¶
PickFile opens an interactive file picker and returns the selected file path. Returns ErrCancelled if the user exits without selecting.
Example ¶
Pick a single Go source file from the user's projects directory.
package main
import (
"fmt"
finder "github.com/rummage-dev/rummage"
)
func main() {
path, err := finder.PickFile(
finder.WithStartDir("~/projects"),
finder.WithFilter("*.go"),
)
if err != nil {
// finder.ErrCancelled is returned if the user exits without selecting.
return
}
fmt.Println("selected:", path)
}
Output:
func PickFolder ¶
PickFolder opens an interactive folder picker and returns the selected directory path. Returns ErrCancelled if the user exits without selecting.
Example ¶
Pick a folder, using the s key inside the picker to confirm the current directory.
package main
import (
"fmt"
finder "github.com/rummage-dev/rummage"
)
func main() {
dir, err := finder.PickFolder()
if err != nil {
return
}
fmt.Println("folder:", dir)
}
Output:
func PickMultiple ¶
PickMultiple opens an interactive multi-select file picker and returns all selected paths. Returns ErrCancelled if the user exits without selecting.
Example ¶
Pick multiple files matching a filter, with hidden files visible. Selections persist across directory navigation.
package main
import (
"fmt"
finder "github.com/rummage-dev/rummage"
)
func main() {
paths, err := finder.PickMultiple(
finder.WithFilter("*.log", "*.txt"),
finder.WithHidden(true),
)
if err != nil {
return
}
for _, p := range paths {
fmt.Println(p)
}
}
Output:
func ResolvePath ¶
ResolvePath evaluates symlinks and returns the real absolute path. If the path cannot be resolved, it falls back to filepath.Abs.
func ToWSLPath ¶
ToWSLPath converts a Windows path like C:\Users to /mnt/c/Users. If the path is not a Windows-style path, it is returned unchanged.
Example ¶
package main
import (
"fmt"
finder "github.com/rummage-dev/rummage"
)
func main() {
fmt.Println(finder.ToWSLPath(`C:\Users\test`))
fmt.Println(finder.ToWSLPath(`D:\data`))
fmt.Println(finder.ToWSLPath("/home/user"))
}
Output: /mnt/c/Users/test /mnt/d/data /home/user
func ToWindowsPath ¶
ToWindowsPath converts a WSL path like /mnt/c/Users to C:\Users. If the path is not a WSL mount path, it is returned unchanged.
Example ¶
package main
import (
"fmt"
finder "github.com/rummage-dev/rummage"
)
func main() {
fmt.Println(finder.ToWindowsPath("/mnt/c/Users/test"))
fmt.Println(finder.ToWindowsPath("/mnt/d/data"))
fmt.Println(finder.ToWindowsPath("/home/user"))
}
Output: C:\Users\test D:\data /home/user
Types ¶
type DoneMsg ¶
DoneMsg is emitted (as a tea.Cmd result) when an embedded picker finishes, so a parent program can react in its own Update. It is not sent in the standalone Pick* API, which ends its own program instead.
type FileEntry ¶
type FileEntry struct {
Name string
Path string
IsDir bool
IsHidden bool
Size int64
Mode os.FileMode
}
FileEntry represents a single file or directory in a listing.
type FileSystem ¶
type FileSystem interface {
// Open opens the named file for reading. This makes a FileSystem usable
// anywhere an fs.FS is expected (e.g. the preview pane).
Open(name string) (fs.File, error)
// ReadDir returns the raw directory entries at the given path.
ReadDir(name string) ([]fs.DirEntry, error)
// Parent returns the parent directory of path. For the root it returns
// path unchanged, which is how the picker detects the top level.
Parent(path string) string
// Base returns the final element of path.
Base(path string) string
// Join appends name to dir, producing a child path in this filesystem's
// path space.
Join(dir, name string) string
// Display renders path for the breadcrumb shown to the user.
Display(path string) string
// Root returns the path the picker starts at when no start directory is
// given.
Root() string
}
FileSystem is the backend the picker reads from and navigates. It abstracts both directory access and path manipulation so the picker can operate on the OS filesystem (the default) or on any custom backend such as an embed.FS or a fstest.MapFS supplied via WithFS.
All path-shaped values the picker stores (the current directory and each FileEntry.Path) are produced by this interface, so the picker never assumes OS path semantics.
type KeyMap ¶
type KeyMap struct {
Up key.Binding
Down key.Binding
PageUp key.Binding
PageDown key.Binding
Home key.Binding
End key.Binding
Back key.Binding
Select key.Binding
SelectDir key.Binding
Toggle key.Binding
ToggleAll key.Binding
Hidden key.Binding
Cancel key.Binding
Search key.Binding
NewFile key.Binding
NewFolder key.Binding
Delete key.Binding
}
KeyMap defines all keybindings for the file picker.
type Mode ¶
type Mode int
Mode defines the picker operation mode.
func (Mode) String ¶
String returns the human-readable name of the mode.
Example ¶
package main
import (
"fmt"
finder "github.com/rummage-dev/rummage"
)
func main() {
fmt.Println(finder.ModeFile)
fmt.Println(finder.ModeFolder)
fmt.Println(finder.ModeAny)
fmt.Println(finder.ModeMultiple)
}
Output: file folder any multiple
type Model ¶
type Model struct {
// contains filtered or unexported fields
}
Model is the bubbletea model for the file picker.
func New ¶
New builds a picker Model for embedding in your own Bubble Tea program. The returned model is in embedded mode: it never calls tea.Quit and instead reports completion via a finder.DoneMsg (and its State method).
Wire it like any Bubble Tea sub-model: call Init and run the returned command, forward messages to Update, render with View, and watch for finder.DoneMsg (or poll State) to read the result with SelectedPath / SelectedPaths.
For a standalone picker that owns the terminal, use PickFile/PickFolder/ PickAny/PickMultiple instead.
Example ¶
Embed the picker as a sub-model in your own Bubble Tea program. New returns a model in embedded mode: forward messages to its Update, render with View, and watch for finder.DoneMsg to read the result. The picker never quits your program itself.
package main
import (
finder "github.com/rummage-dev/rummage"
)
func main() {
picker := finder.New(
finder.WithMode(finder.ModeFile),
finder.WithTitle("Pick a file"),
)
// In your parent model's Update, forward messages and handle completion:
//
// func (m parent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// if done, ok := msg.(finder.DoneMsg); ok {
// if done.State == finder.StateSelected {
// m.result = done.Paths
// }
// return m, nil
// }
// updated, cmd := m.picker.Update(msg)
// m.picker = updated.(finder.Model)
// return m, cmd
// }
_ = picker.Init()
}
Output:
func (Model) Done ¶
Done reports whether the user has finished with the picker (either selected or cancelled).
func (Model) SelectedPath ¶
SelectedPath returns the single selected path after the picker closes. Returns empty string if nothing was selected.
func (Model) SelectedPaths ¶
SelectedPaths returns all selected paths (for multi-select mode). Paths are returned in sorted order for deterministic results.
func (Model) State ¶
State returns the picker's lifecycle state. Embedded parents use it (or watch for DoneMsg) to detect when the user has finished.
type Option ¶
type Option func(*Options)
Option is a functional option for configuring the picker.
func WithEmbedded ¶
WithEmbedded marks the picker for embedding in a larger Bubble Tea program, so it signals completion via DoneMsg / State() instead of quitting. New sets this automatically; use it directly only with NewModel.
func WithExpandSymlinks ¶
WithExpandSymlinks resolves symlinks to their real paths. When enabled, the picker operates on resolved paths so navigating back from a symlinked directory goes to the real parent rather than the symlink's parent.
func WithExtensions ¶
WithExtensions limits visible files to the given extensions, matched case-insensitively. Extensions may be given with or without a leading dot ("pdf" or ".pdf"), and an optional leading glob star is tolerated ("*.pdf"), so a file named "Report.PDF" is shown by WithExtensions("pdf"). Directories are always shown regardless of extensions. Blank entries are ignored.
WithExtensions composes with WithFilter: when both are set, a file is shown if it matches either the glob patterns or the extensions.
Example ¶
Restrict the picker to a set of document extensions. Matching is case-insensitive, so "Report.PDF" is shown by WithExtensions("pdf").
package main
import (
"fmt"
finder "github.com/rummage-dev/rummage"
)
func main() {
path, err := finder.PickFile(
finder.WithExtensions("pdf", "docx", "doc"),
)
if err != nil {
return
}
fmt.Println("document:", path)
}
Output:
func WithFS ¶
WithFS sets a custom filesystem backend to browse instead of the host OS. The picker navigates the given io/fs.FS in slash-separated path space, starting at its root ("."). Because io/fs is read-only, interactive create/delete is unavailable on a custom filesystem.
To start in a subdirectory, combine with WithStartDir using a relative, slash-separated path (e.g. WithStartDir("docs/api")).
Example ¶
Browse an in-memory filesystem instead of the host OS. Any io/fs.FS works, including embed.FS and fstest.MapFS. Custom filesystems are read-only, so interactive create/delete is disabled for them.
package main
import (
"fmt"
"testing/fstest"
finder "github.com/rummage-dev/rummage"
)
func main() {
mem := fstest.MapFS{
"docs/readme.md": {Data: []byte("# hello")},
"main.go": {Data: []byte("package main")},
}
path, err := finder.PickFile(finder.WithFS(mem))
if err != nil {
return
}
fmt.Println("selected:", path)
}
Output:
func WithFilter ¶
WithFilter adds glob patterns to limit which files are shown. Directories are always shown regardless of filters.
func WithFuzzySearch ¶
WithFuzzySearch toggles scored fuzzy matching for the search filter. When enabled (the default), results are ranked best-match-first. When disabled, search falls back to a case-insensitive substring match that preserves the original directory ordering.
func WithHeight ¶
WithHeight sets the maximum visible rows for the file list. A value of 0 means auto-detect from terminal size.
func WithHidden ¶
WithHidden controls whether hidden files and directories are shown.
func WithInteractive ¶
WithInteractive enables file management actions (create files/folders, delete).
func WithKeyMap ¶
WithKeyMap overrides the default keybindings. All fields of the KeyMap struct are public, so callers can customize any or all bindings.
Example ¶
Rebind the cancel key from q to x.
package main
import (
"github.com/charmbracelet/bubbles/key"
finder "github.com/rummage-dev/rummage"
)
func main() {
km := finder.DefaultKeyMap()
km.Cancel = key.NewBinding(
key.WithKeys("x"),
key.WithHelp("x", "exit"),
)
_, _ = finder.PickFile(finder.WithKeyMap(km))
}
Output:
func WithPreview ¶
WithPreview enables (or disables) the preview pane shown beside the file list. The pane is hidden automatically when the terminal is too narrow.
Example ¶
Show a preview pane beside the file list. The pane previews the highlighted entry (file head, directory listing, or metadata) and is hidden automatically on narrow terminals.
package main
import (
"fmt"
finder "github.com/rummage-dev/rummage"
)
func main() {
path, err := finder.PickFile(finder.WithPreview(true))
if err != nil {
return
}
fmt.Println("selected:", path)
}
Output:
func WithPreviewFunc ¶
func WithPreviewFunc(fn PreviewFunc) Option
WithPreviewFunc sets a custom preview renderer and enables the preview pane. Pass nil to fall back to the built-in preview while keeping the pane enabled.
Example ¶
Supply a custom preview renderer. The function receives the highlighted entry and the pane dimensions, and returns the text to display.
package main
import (
"fmt"
finder "github.com/rummage-dev/rummage"
)
func main() {
_, _ = finder.PickFile(finder.WithPreviewFunc(
func(e finder.FileEntry, width, height int) string {
return fmt.Sprintf("%s\n%s", e.Name, finder.FormatSize(e.Size))
},
))
}
Output:
func WithStartDir ¶
WithStartDir sets the initial directory for the picker. For the default OS backend the path is resolved to an absolute path when the picker starts; for a custom filesystem (see WithFS) it is interpreted as a relative, slash-separated path within that filesystem.
func WithStyles ¶
WithStyles overrides the default visual styles.
Example ¶
Override visual styles for directory entries.
package main
import (
"github.com/charmbracelet/lipgloss"
finder "github.com/rummage-dev/rummage"
)
func main() {
s := finder.DefaultStyles()
s.Directory = lipgloss.NewStyle().
Foreground(lipgloss.Color("82")).
Bold(true)
_, _ = finder.PickFile(finder.WithStyles(s))
}
Output:
type Options ¶
type Options struct {
// StartDir is the initial directory shown in the picker.
StartDir string
// Filters is a list of glob patterns to filter visible files (e.g. "*.go", "*.txt").
// Matching is case-sensitive (see filepath.Match). Empty means show all files.
Filters []string
// Extensions limits visible files to the given extensions, matched
// case-insensitively (e.g. ".pdf", ".docx"). Values are normalized to a
// lowercase leading-dot form. Empty means no extension restriction. When
// both Filters and Extensions are set, a file is shown if it matches
// either one.
Extensions []string
// ShowHidden controls whether dotfiles/dotfolders are displayed.
ShowHidden bool
// Mode determines the picker behavior (file, folder, or multi-select).
Mode Mode
// Title is the header text displayed at the top of the picker.
Title string
// Height constrains the maximum number of rows in the file list.
// Zero means auto-detect from terminal size.
Height int
// Interactive enables file management actions (create, delete).
Interactive bool
// ExpandSymlinks resolves symlinks to their real paths. When enabled,
// navigating back from a symlinked directory goes to the real parent
// rather than the symlink's parent.
ExpandSymlinks bool
// FuzzySearch enables scored fuzzy matching for the search filter,
// ranking results best-match-first. When false, search uses a plain
// case-insensitive substring match that preserves the original order.
FuzzySearch bool
// Embedded marks the picker as a sub-model of a larger Bubble Tea program.
// When set, the picker never emits tea.Quit; instead it signals completion
// via DoneMsg and its State() method, leaving control of the program to the
// parent. The Pick* one-liner API leaves this false. See New.
Embedded bool
// Preview enables a right-hand pane that previews the highlighted entry
// (file head, directory listing, or metadata). Off by default.
Preview bool
// PreviewFunc customizes how the highlighted entry is previewed. When nil,
// a built-in preview is used. It receives the entry and the pane's
// width/height (in cells) and returns the preview text; the result is
// clipped to fit the pane.
PreviewFunc PreviewFunc
// FS is the filesystem backend the picker reads from. Nil means the
// host operating system. Set it via WithFS to browse a custom io/fs.FS
// (e.g. an embed.FS or fstest.MapFS); such filesystems are read-only,
// so interactive create/delete is disabled for them.
FS FileSystem
// KeyMap overrides the default keybindings. Nil means use defaults.
KeyMap *KeyMap
// Styles overrides the default visual styles. Nil means use defaults.
Styles *Styles
}
Options holds the configuration for a picker session.
func DefaultOptions ¶
func DefaultOptions() Options
DefaultOptions returns a new Options struct with sensible defaults.
type PreviewFunc ¶
PreviewFunc renders a preview of the highlighted entry. It is given the entry and the preview pane's width and height in terminal cells, and returns the preview text (which may span multiple lines). The returned text is clipped to the pane's dimensions.
type State ¶
type State int
State describes where the picker is in its lifecycle. It lets a parent program that embeds the picker (see New) detect when the user has finished.
const ( // StateBrowsing means the user is still navigating and has not finished. StateBrowsing State = iota // StateSelected means the user confirmed a selection; read it with // SelectedPath or SelectedPaths. StateSelected // StateCancelled means the user dismissed the picker without selecting. StateCancelled )
type Styles ¶
type Styles struct {
Title lipgloss.Style
Path lipgloss.Style
Cursor lipgloss.Style
Selected lipgloss.Style
Directory lipgloss.Style
HiddenDir lipgloss.Style
File lipgloss.Style
HiddenFile lipgloss.Style
FileSize lipgloss.Style
Permission lipgloss.Style
StatusBar lipgloss.Style
SearchPrompt lipgloss.Style
SearchText lipgloss.Style
Help lipgloss.Style
HelpKey lipgloss.Style
HelpDesc lipgloss.Style
HelpSep lipgloss.Style
Border lipgloss.Style
Preview lipgloss.Style
PreviewBorder lipgloss.Style
Match lipgloss.Style
}
Styles holds all visual styles for the picker UI.
func DefaultStyles ¶
func DefaultStyles() Styles
DefaultStyles returns the default color scheme and styling.
type WritableFileSystem ¶
type WritableFileSystem interface {
FileSystem
CreateFile(dir, name string) error
CreateDir(dir, name string) error
Remove(path string) error
}
WritableFileSystem is the optional extension a FileSystem implements to support interactive mode (creating and deleting entries). A FileSystem that does not implement it is treated as read-only: the create/delete keys show a status message instead of acting.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
basic
command
Basic example demonstrates all rummage picker modes with flag-based configuration.
|
Basic example demonstrates all rummage picker modes with flag-based configuration. |
|
custom
command
Custom example shows how to override keybindings and styles.
|
Custom example shows how to override keybindings and styles. |
|
embedded
command
Embedded example shows how to embed the picker as a sub-model inside your own Bubble Tea program, rather than using the standalone Pick* API.
|
Embedded example shows how to embed the picker as a sub-model inside your own Bubble Tea program, rather than using the standalone Pick* API. |
|
extensions
command
Extensions example shows how to restrict the picker to a set of file extensions, the way many "attach a document" dialogs only allow .pdf, .docx, .doc, and so on.
|
Extensions example shows how to restrict the picker to a set of file extensions, the way many "attach a document" dialogs only allow .pdf, .docx, .doc, and so on. |
|
folder
command
Folder example shows how to use rummage as a directory picker.
|
Folder example shows how to use rummage as a directory picker. |
|
interactive
command
Interactive example shows how to use rummage with file management actions enabled (create files/folders, delete entries).
|
Interactive example shows how to use rummage with file management actions enabled (create files/folders, delete entries). |
|
multi
command
Multi example shows how to use rummage for multi-file selection.
|
Multi example shows how to use rummage for multi-file selection. |
|
preview
command
Preview example shows the optional preview pane, which previews the highlighted entry (a file's head, a directory's listing, or metadata) beside the file list.
|
Preview example shows the optional preview pane, which previews the highlighted entry (a file's head, a directory's listing, or metadata) beside the file list. |