diff --git a/src/data.go b/src/data.go index 81c05a2..6f8efd8 100644 --- a/src/data.go +++ b/src/data.go @@ -4,55 +4,57 @@ import ( "bufio" "fmt" "os" + "path/filepath" "sort" "strings" - "sync" + "sync" + "github.com/charmbracelet/bubbles/list" ) type Ranking struct { - items []string - indexes []int - scores []float64 + items []string + indexes []int + scores []float64 } func (r Ranking) Len() int { return len(r.items) } 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 { - 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 func computeScore(term string, s string) float64 { - term = strings.ToLower(term) - a := strings.ToLower(s) - splits := strings.Split(a, "/") - term_splits := strings.Split(term, "/") + term = strings.ToLower(term) + a := strings.ToLower(s) + splits := strings.Split(a, "/") + term_splits := strings.Split(term, "/") - score := 0.0 - used := make([]bool, len(term_splits)) - for i, term := range term_splits { - if used[i] { - continue - } - for j, c := range splits { - if len(term) == 0 { - continue - } - if strings.Contains(c, term) { - used[i] = true - score += 1.0/float64(j) - splits = splits[j+1:] - break - } - } - } - if score > 0 { - score += 1.0/float64(strings.Count(a, "/")+1) - } - return score + score := 0.0 + used := make([]bool, len(term_splits)) + for i, term := range term_splits { + if used[i] { + continue + } + for j, c := range splits { + if len(term) == 0 { + continue + } + if strings.Contains(c, term) { + used[i] = true + score += 1.0 / float64(j) + splits = splits[j+1:] + break + } + } + } + if score > 0 { + score += 1.0 / float64(strings.Count(a, "/")+1) + } + return score } func rank(term string, targets []string) []int { @@ -88,17 +90,17 @@ func rank(term string, targets []string) []int { scores[result.index] = result.score } - indexes := make([]int, len(targets)) - for i := range targets { - indexes[i] = i - } + indexes := make([]int, len(targets)) + for i := range targets { + indexes[i] = i + } - sort.Stable(Ranking{targets, indexes, scores}) - return indexes + sort.Stable(Ranking{targets, indexes, scores}) + return indexes } func Filter(term string, targets []string) []list.Rank { - indexes := rank(term, targets) + indexes := rank(term, targets) result := make([]list.Rank, len(targets)) for i := range targets { result[i] = list.Rank{ @@ -110,58 +112,110 @@ func Filter(term string, targets []string) []list.Rank { } func getPathsFromFile(path string) []string { - paths := []string{} + paths := []string{} - file, err := os.Open(path) - if err != nil { - fmt.Fprintln(os.Stderr, "Error opening file:", err) - return paths - } - defer file.Close() // Ensure the file is closed when we're done + file, err := os.Open(path) + if err != nil { + fmt.Fprintln(os.Stderr, "Error opening file:", err) + return paths + } + defer file.Close() // Ensure the file is closed when we're done - // Create a scanner to read the file line by line - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if len(line) == 0 { - continue - } - paths = append(paths, line) - } + // Create a scanner to read the file line by line + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if len(line) == 0 { + continue + } + paths = append(paths, line) + } - // Check for errors during scanning - if err := scanner.Err(); err != nil { - fmt.Fprintln(os.Stderr, "Error reading file:", err) - } - return paths + // Check for errors during scanning + if err := scanner.Err(); err != nil { + fmt.Fprintln(os.Stderr, "Error reading file:", err) + } + return paths } func getPathsFromStdin() []string { - paths := []string{} + paths := []string{} scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { line := scanner.Text() // Get the current line - paths = append(paths, line) + paths = append(paths, line) } // Check for errors during scanning if err := scanner.Err(); err != nil { 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 { - 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 + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + // Skip hidden files and directories (those starting with a dot) + baseName := filepath.Base(path) + is_dotfile := strings.HasPrefix(baseName, ".") + if info.IsDir() { + if is_dotfile && filepath.Clean(path) != filepath.Clean(root) { + return filepath.SkipDir + } + 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 } diff --git a/src/main.go b/src/main.go index d179082..ef8aa23 100644 --- a/src/main.go +++ b/src/main.go @@ -1,22 +1,48 @@ package main import ( + "flag" "fmt" "log" "os" + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/muesli/termenv" ) func main() { - lipgloss.DefaultRenderer().SetColorProfile(termenv.TrueColor) - p := tea.NewProgram(initialModel(getListItems()), tea.WithOutput(os.Stderr)) - m, err := p.Run(); + lipgloss.DefaultRenderer().SetColorProfile(termenv.TrueColor) + + // 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 { log.Fatal(err) } else { - fmt.Println(m.View()) - } + fmt.Println(m.View()) + } } diff --git a/src/tui.go b/src/tui.go index 58c0ea2..edb69f3 100644 --- a/src/tui.go +++ b/src/tui.go @@ -2,20 +2,21 @@ package main import ( "fmt" - "io" + "io" "strings" + "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/lipgloss" ) - type item struct { - value string + value string } + func (i item) FilterValue() string { return i.value } + type itemDelegate struct{} 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 } - str := fmt.Sprintf("%s", i.value) + str := i.value fn := lipgloss.NewStyle().PaddingLeft(2).Render 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) { var cmd tea.Cmd - ignoreInput := false + ignoreInput := false switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.Type { - case tea.KeyCtrlC, tea.KeyEsc: - m.quitting = true; - return m, tea.Quit - case tea.KeyEnter: - i, ok := m.list.SelectedItem().(item) - if ok { - m.choice = i.value - } - return m, tea.Quit - case tea.KeyUp, tea.KeyCtrlK: - m.list.CursorUp() - ignoreInput = true - case tea.KeyDown, tea.KeyCtrlJ: - m.list.CursorDown() - ignoreInput = true - } + case tea.KeyMsg: + switch msg.Type { + case tea.KeyCtrlC, tea.KeyEsc: + m.quitting = true + return m, tea.Quit + case tea.KeyEnter: + i, ok := m.list.SelectedItem().(item) + if ok { + m.choice = i.value + } + return m, tea.Quit + case tea.KeyUp, tea.KeyCtrlK: + m.list.CursorUp() + ignoreInput = true + case tea.KeyDown, tea.KeyCtrlJ: + m.list.CursorDown() + ignoreInput = true + } - // We handle errors just like any other message - case errMsg: - m.err = msg - return m, nil + // We handle errors just like any other message + case errMsg: + m.err = msg + return m, nil - case tea.WindowSizeMsg: - m.textInput.Width = msg.Width - m.list.SetWidth(msg.Width) - return m, nil - } + case tea.WindowSizeMsg: + m.textInput.Width = msg.Width + m.list.SetWidth(msg.Width) + return m, nil + } - if ignoreInput { - return m, cmd - } - cmds := []tea.Cmd{} + if ignoreInput { + return m, cmd + } + cmds := []tea.Cmd{} m.textInput, cmd = m.textInput.Update(msg) - cmds = append(cmds, cmd) + cmds = append(cmds, cmd) - - if !m.list.SettingFilter() { - m.list, cmd = m.list.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}) - cmds = append(cmds, cmd) - } + if !m.list.SettingFilter() { + m.list, cmd = m.list.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}) + cmds = append(cmds, cmd) + } m.list, cmd = m.list.Update(msg) - cmds = append(cmds, cmd) + cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } func (m model) View() string { if m.choice != "" { - return m.choice + return m.choice } if m.quitting { return "" } return fmt.Sprintf("%s\n", - m.list.View(), + m.list.View(), ) } - type ( errMsg error ) type model struct { textInput textinput.Model - list list.Model - choice string - quitting bool + list list.Model + choice string + quitting bool err error } @@ -122,23 +121,23 @@ func initialModel(items []list.Item) model { const defaultWidth = 20 l := list.New(items, itemDelegate{}, defaultWidth, 14) - l.SetShowPagination(false) + l.SetShowPagination(false) l.SetShowStatusBar(false) - l.SetShowHelp(false) - l.SetShowTitle(false) + l.SetShowHelp(false) + l.SetShowTitle(false) l.SetFilteringEnabled(true) - l.SetShowFilter(true) - l.Styles.TitleBar = lipgloss.NewStyle() + l.SetShowFilter(true) + l.Styles.TitleBar = lipgloss.NewStyle() ti := textinput.New() ti.Focus() ti.CharLimit = 4096 ti.Width = 20 - l.FilterInput = ti - l.Filter = Filter + l.FilterInput = ti + l.Filter = Filter return model{ - list: l, - err: nil, + list: l, + err: nil, } }