Add option to search for directories through fzgo itself

This commit is contained in:
Thomas Avé 2025-01-27 16:22:38 +01:00
parent 430ff540f5
commit a005938761
3 changed files with 218 additions and 139 deletions

View File

@ -4,55 +4,57 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"os" "os"
"path/filepath"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/list"
) )
type Ranking struct { type Ranking struct {
items []string items []string
indexes []int indexes []int
scores []float64 scores []float64
} }
func (r Ranking) Len() int { return len(r.items) } func (r Ranking) Len() int { return len(r.items) }
func (r Ranking) Swap(i, j int) { func (r Ranking) Swap(i, j int) {
r.indexes[i], r.indexes[j] = r.indexes[j], r.indexes[i] r.indexes[i], r.indexes[j] = r.indexes[j], r.indexes[i]
} }
func (r Ranking) Less(i, j int) bool { func (r Ranking) Less(i, j int) bool {
return r.scores[r.indexes[i]] > r.scores[r.indexes[j]] return r.scores[r.indexes[i]] > r.scores[r.indexes[j]]
} }
// Function to compute the score for a given string // Function to compute the score for a given string
func computeScore(term string, s string) float64 { func computeScore(term string, s string) float64 {
term = strings.ToLower(term) term = strings.ToLower(term)
a := strings.ToLower(s) a := strings.ToLower(s)
splits := strings.Split(a, "/") splits := strings.Split(a, "/")
term_splits := strings.Split(term, "/") term_splits := strings.Split(term, "/")
score := 0.0 score := 0.0
used := make([]bool, len(term_splits)) used := make([]bool, len(term_splits))
for i, term := range term_splits { for i, term := range term_splits {
if used[i] { if used[i] {
continue continue
} }
for j, c := range splits { for j, c := range splits {
if len(term) == 0 { if len(term) == 0 {
continue continue
} }
if strings.Contains(c, term) { if strings.Contains(c, term) {
used[i] = true used[i] = true
score += 1.0/float64(j) score += 1.0 / float64(j)
splits = splits[j+1:] splits = splits[j+1:]
break break
} }
} }
} }
if score > 0 { if score > 0 {
score += 1.0/float64(strings.Count(a, "/")+1) score += 1.0 / float64(strings.Count(a, "/")+1)
} }
return score return score
} }
func rank(term string, targets []string) []int { func rank(term string, targets []string) []int {
@ -88,17 +90,17 @@ func rank(term string, targets []string) []int {
scores[result.index] = result.score scores[result.index] = result.score
} }
indexes := make([]int, len(targets)) indexes := make([]int, len(targets))
for i := range targets { for i := range targets {
indexes[i] = i indexes[i] = i
} }
sort.Stable(Ranking{targets, indexes, scores}) sort.Stable(Ranking{targets, indexes, scores})
return indexes return indexes
} }
func Filter(term string, targets []string) []list.Rank { func Filter(term string, targets []string) []list.Rank {
indexes := rank(term, targets) indexes := rank(term, targets)
result := make([]list.Rank, len(targets)) result := make([]list.Rank, len(targets))
for i := range targets { for i := range targets {
result[i] = list.Rank{ result[i] = list.Rank{
@ -110,58 +112,110 @@ func Filter(term string, targets []string) []list.Rank {
} }
func getPathsFromFile(path string) []string { func getPathsFromFile(path string) []string {
paths := []string{} paths := []string{}
file, err := os.Open(path) file, err := os.Open(path)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "Error opening file:", err) fmt.Fprintln(os.Stderr, "Error opening file:", err)
return paths return paths
} }
defer file.Close() // Ensure the file is closed when we're done defer file.Close() // Ensure the file is closed when we're done
// Create a scanner to read the file line by line // Create a scanner to read the file line by line
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
if len(line) == 0 { if len(line) == 0 {
continue continue
} }
paths = append(paths, line) paths = append(paths, line)
} }
// Check for errors during scanning // Check for errors during scanning
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "Error reading file:", err) fmt.Fprintln(os.Stderr, "Error reading file:", err)
} }
return paths return paths
} }
func getPathsFromStdin() []string { func getPathsFromStdin() []string {
paths := []string{} paths := []string{}
scanner := bufio.NewScanner(os.Stdin) scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() // Get the current line line := scanner.Text() // Get the current line
paths = append(paths, line) paths = append(paths, line)
} }
// Check for errors during scanning // Check for errors during scanning
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "Error reading from stdin:", err) fmt.Fprintln(os.Stderr, "Error reading from stdin:", err)
} }
return paths return paths
} }
func findSubdirectories(root string) ([]string, []string, error) {
var dirs []string
var files []string
func getListItems() []list.Item { err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
items := []list.Item{} if err != nil {
// paths := getPathsFromFile("/home/user/.cache/fzy_paths_d") return err
paths := getPathsFromStdin() }
for _, path := range paths { // Skip hidden files and directories (those starting with a dot)
if len(path) == 0 { baseName := filepath.Base(path)
continue is_dotfile := strings.HasPrefix(baseName, ".")
} if info.IsDir() {
items = append(items, item{path}) if is_dotfile && filepath.Clean(path) != filepath.Clean(root) {
} return filepath.SkipDir
return items }
dirs = append(dirs, path)
} else {
if is_dotfile {
return nil
}
files = append(files, path)
}
return nil
})
if err != nil {
return nil, nil, fmt.Errorf("error walking directory tree: %v", err)
}
return dirs, files, nil
}
func getListItemsFromDir(root string, searchDirs bool, searchFiles bool) []list.Item {
items := []list.Item{}
dirs, files, err := findSubdirectories(root)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return items
}
if searchFiles {
for _, file := range files {
items = append(items, item{file})
}
}
if searchDirs {
for _, dir := range dirs {
items = append(items, item{dir})
}
}
return items
}
func getListItemsFromStdin() []list.Item {
items := []list.Item{}
// paths := getPathsFromFile("/home/user/.cache/fzy_paths_d")
paths := getPathsFromStdin()
for _, path := range paths {
if len(path) == 0 {
continue
}
items = append(items, item{path})
}
return items
} }

View File

@ -1,22 +1,48 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"log" "log"
"os" "os"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv" "github.com/muesli/termenv"
) )
func main() { func main() {
lipgloss.DefaultRenderer().SetColorProfile(termenv.TrueColor) lipgloss.DefaultRenderer().SetColorProfile(termenv.TrueColor)
p := tea.NewProgram(initialModel(getListItems()), tea.WithOutput(os.Stderr))
m, err := p.Run(); // Command-line flags
searchFiles := flag.Bool("f", false, "Search for files")
searchDirs := flag.Bool("d", false, "Search for directories")
flag.Parse()
if !*searchFiles && !*searchDirs {
*searchFiles = true
*searchDirs = true
}
// Gather list items for each path
var items []list.Item
// Read paths from command line arguments
if len(flag.Args()) < 1 {
items = getListItemsFromStdin()
} else {
paths := flag.Args()
for _, path := range paths {
items = append(items, getListItemsFromDir(path, *searchDirs, *searchFiles)...)
}
}
p := tea.NewProgram(initialModel(items), tea.WithOutput(os.Stderr))
m, err := p.Run()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} else { } else {
fmt.Println(m.View()) fmt.Println(m.View())
} }
} }

View File

@ -2,20 +2,21 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"strings" "strings"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/list"
) )
type item struct { type item struct {
value string value string
} }
func (i item) FilterValue() string { return i.value } func (i item) FilterValue() string { return i.value }
type itemDelegate struct{} type itemDelegate struct{}
func (d itemDelegate) Height() int { return 1 } func (d itemDelegate) Height() int { return 1 }
@ -27,7 +28,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
return return
} }
str := fmt.Sprintf("%s", i.value) str := i.value
fn := lipgloss.NewStyle().PaddingLeft(2).Render fn := lipgloss.NewStyle().PaddingLeft(2).Render
if index == m.Index() { if index == m.Index() {
@ -41,80 +42,78 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd var cmd tea.Cmd
ignoreInput := false ignoreInput := false
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.Type { switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc: case tea.KeyCtrlC, tea.KeyEsc:
m.quitting = true; m.quitting = true
return m, tea.Quit return m, tea.Quit
case tea.KeyEnter: case tea.KeyEnter:
i, ok := m.list.SelectedItem().(item) i, ok := m.list.SelectedItem().(item)
if ok { if ok {
m.choice = i.value m.choice = i.value
} }
return m, tea.Quit return m, tea.Quit
case tea.KeyUp, tea.KeyCtrlK: case tea.KeyUp, tea.KeyCtrlK:
m.list.CursorUp() m.list.CursorUp()
ignoreInput = true ignoreInput = true
case tea.KeyDown, tea.KeyCtrlJ: case tea.KeyDown, tea.KeyCtrlJ:
m.list.CursorDown() m.list.CursorDown()
ignoreInput = true ignoreInput = true
} }
// We handle errors just like any other message // We handle errors just like any other message
case errMsg: case errMsg:
m.err = msg m.err = msg
return m, nil return m, nil
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.textInput.Width = msg.Width m.textInput.Width = msg.Width
m.list.SetWidth(msg.Width) m.list.SetWidth(msg.Width)
return m, nil return m, nil
} }
if ignoreInput { if ignoreInput {
return m, cmd return m, cmd
} }
cmds := []tea.Cmd{} cmds := []tea.Cmd{}
m.textInput, cmd = m.textInput.Update(msg) m.textInput, cmd = m.textInput.Update(msg)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
if !m.list.SettingFilter() {
if !m.list.SettingFilter() { m.list, cmd = m.list.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")})
m.list, cmd = m.list.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}) cmds = append(cmds, cmd)
cmds = append(cmds, cmd) }
}
m.list, cmd = m.list.Update(msg) m.list, cmd = m.list.Update(msg)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
} }
func (m model) View() string { func (m model) View() string {
if m.choice != "" { if m.choice != "" {
return m.choice return m.choice
} }
if m.quitting { if m.quitting {
return "" return ""
} }
return fmt.Sprintf("%s\n", return fmt.Sprintf("%s\n",
m.list.View(), m.list.View(),
) )
} }
type ( type (
errMsg error errMsg error
) )
type model struct { type model struct {
textInput textinput.Model textInput textinput.Model
list list.Model list list.Model
choice string choice string
quitting bool quitting bool
err error err error
} }
@ -122,23 +121,23 @@ func initialModel(items []list.Item) model {
const defaultWidth = 20 const defaultWidth = 20
l := list.New(items, itemDelegate{}, defaultWidth, 14) l := list.New(items, itemDelegate{}, defaultWidth, 14)
l.SetShowPagination(false) l.SetShowPagination(false)
l.SetShowStatusBar(false) l.SetShowStatusBar(false)
l.SetShowHelp(false) l.SetShowHelp(false)
l.SetShowTitle(false) l.SetShowTitle(false)
l.SetFilteringEnabled(true) l.SetFilteringEnabled(true)
l.SetShowFilter(true) l.SetShowFilter(true)
l.Styles.TitleBar = lipgloss.NewStyle() l.Styles.TitleBar = lipgloss.NewStyle()
ti := textinput.New() ti := textinput.New()
ti.Focus() ti.Focus()
ti.CharLimit = 4096 ti.CharLimit = 4096
ti.Width = 20 ti.Width = 20
l.FilterInput = ti l.FilterInput = ti
l.Filter = Filter l.Filter = Filter
return model{ return model{
list: l, list: l,
err: nil, err: nil,
} }
} }