Skip to content

Commit

Permalink
feat: fuzzy search with fzf (#417)
Browse files Browse the repository at this point in the history
  • Loading branch information
GianlucaP106 authored Jan 11, 2025
1 parent 440913c commit ff8918a
Show file tree
Hide file tree
Showing 11 changed files with 140 additions and 73 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module mynav
go 1.22.3

require (
github.com/GianlucaP106/gotmux v0.2.1-0.20240906010315-bb38e85334e8
github.com/GianlucaP106/gotmux v0.2.1-0.20250111211312-a5a2e886166e
github.com/atotto/clipboard v0.1.4
github.com/awesome-gocui/gocui v1.1.0
github.com/gookit/color v1.5.4
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/GianlucaP106/gotmux v0.2.1-0.20240906010315-bb38e85334e8 h1:VOR/nn5FQLFGU6g5p9kXi9UmW3GGsZFvdXBWFggD6BI=
github.com/GianlucaP106/gotmux v0.2.1-0.20240906010315-bb38e85334e8/go.mod h1:qOsZ+exnCbgv3KJ84VaBo4Q7mXs/W23CW4fyoXAgKe4=
github.com/GianlucaP106/gotmux v0.2.1-0.20250111211312-a5a2e886166e h1:wzaseCPcL+c7MMfd828YTIjnwN8xGvCpcgM/ZcO3vdQ=
github.com/GianlucaP106/gotmux v0.2.1-0.20250111211312-a5a2e886166e/go.mod h1:qOsZ+exnCbgv3KJ84VaBo4Q7mXs/W23CW4fyoXAgKe4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/awesome-gocui/gocui v1.1.0 h1:db2j7yFEoHZjpQFeE2xqiatS8bm1lO3THeLwE6MzOII=
Expand Down
79 changes: 51 additions & 28 deletions pkg/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log"
"mynav/pkg/core"
"mynav/pkg/system"
"mynav/pkg/tui"
"os"
"sync/atomic"
Expand Down Expand Up @@ -67,6 +68,7 @@ var a *App

// Inits and starts the app.
func Start() {
system.InitLogger("debug.log")
a = newApp()
a.start()
}
Expand Down Expand Up @@ -464,39 +466,60 @@ func (a *App) initGlobalKeys() {
return
}

sd := new(*Search[*core.Workspace])
*sd = search(SearchDialogConfig[*core.Workspace]{
onSearch: func(s string) ([][]string, []*core.Workspace) {
// get all workspaces by name containing
allWorkspaces := a.api.AllWorkspaces()
workspaces := allWorkspaces.ByNameContaining(s)

// get all topics with this name containing
topics := a.api.AllTopics().ByNameContaining(s)

// collect all workspaces for each of these topics
for _, t := range topics {
ws := allWorkspaces.ByTopic(t)
workspaces = append(workspaces, ws...)
// TODO: move to seperate file

useFzf := false
if system.IsFzfInstalled() {
useFzf = true
} else {
toast("install fzf it for a better experience", toastWarn)
}

// make helper function to create rows from workspaces
makeRows := func(workspaces core.Workspaces) [][]string {
rows := make([][]string, 0)
for _, w := range workspaces {
session := a.api.Session(w)
sessionStr := ""
if session != nil {
sessionStr = "Yes"
}
rows = append(rows, []string{
w.Name,
w.Topic.Name,
sessionStr,
})
}
return rows
}

workspaces = workspaces.RemoveDuplicates().SortedByTopic()
rows := make([][]string, 0)
for _, w := range workspaces {
session := a.api.Session(w)
sessionStr := ""
if session != nil {
sessionStr = "Yes"
searchFor := func(s string) ([][]string, []*core.Workspace) {
allWorkspaces := a.api.AllWorkspaces().Sorted()
foundWorkspaces := make(core.Workspaces, 0)
if useFzf {
found := []string{}
allNames := []string{}
for _, w := range allWorkspaces {
allNames = append(allNames, w.ShortPath())
}
found = system.FuzzyFind(allNames, s)
for _, item := range found {
w := a.api.FindWorkspaceByShortPath(item)
if w != nil {
foundWorkspaces = append(foundWorkspaces, w)
}
rows = append(rows, []string{
w.Name,
w.Topic.Name,
sessionStr,
})
}
} else {
foundWorkspaces = allWorkspaces.ByNameContaining(s)
}

return rows, workspaces
},
return makeRows(foundWorkspaces), foundWorkspaces
}

sd := new(*Search[*core.Workspace])
*sd = search(SearchDialogConfig[*core.Workspace]{
onType: searchFor,
onSearch: searchFor,
onSelect: func(w *core.Workspace) {
a.topics.selectTopic(w.Topic)
a.workspaces.refresh()
Expand Down
2 changes: 1 addition & 1 deletion pkg/app/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func editor(onEnter func(string), onEsc func(), title string, size editorSize, d
a.ui.FocusView(prevView)
}
onEsc()
})
}, nil)

a.ui.FocusView(ed.view)

Expand Down
28 changes: 25 additions & 3 deletions pkg/app/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Search[T any] struct {
type SearchDialogConfig[T any] struct {
onSearch func(s string) ([][]string, []T)
onSelect func(a T)
onType func(string) ([][]string, []T)
initial func() ([][]string, []T)
onSelectDescription string
searchViewTitle string
Expand All @@ -33,21 +34,34 @@ type SearchDialogConfig[T any] struct {
func search[T any](params SearchDialogConfig[T]) *Search[T] {
// build search view
s := &Search[T]{}
s.searchView = a.ui.SetCenteredView(SearchListDialog1View, 80, 3, -7)
s.searchView = a.ui.SetCenteredView(SearchListDialog1View, 120, 3, -9)
s.searchView.Title = fmt.Sprintf(" %s ", params.searchViewTitle)
s.searchView.Subtitle = " <Enter> to filter "
s.searchView.Editable = true
a.styleView(s.searchView)

var onType func(s string) = nil
if params.onType != nil {
onType = func(search string) {
a.worker.Queue(func() {
rows, vals := params.onType(search)
s.table.Fill(rows, vals)
a.ui.Update(func() {
s.renderTable()
})
})
}
}
s.searchView.Editor = tui.NewSimpleEditor(func(item string) {
rows, rowValues := params.onSearch(item)
s.table.Fill(rows, rowValues)
s.renderTable()
s.focusList()
}, func() {
})
}, onType)

// build table view
s.tableView = a.ui.SetCenteredView(SearchListDialog2View, 80, 10, 0)
s.tableView = a.ui.SetCenteredView(SearchListDialog2View, 120, 15, 0)
s.tableView.Title = fmt.Sprintf(" %s ", params.tableViewTitle)
s.tableView.Subtitle = " <Tab> to toggle focus "
tableViewX, tableViewY := s.tableView.Size()
Expand Down Expand Up @@ -109,6 +123,14 @@ func search[T any](params SearchDialogConfig[T]) *Search[T] {
s.table.Up()
s.renderTable()
}).
Set('g', "Go to top", func() {
s.table.Top()
s.renderTable()
}).
Set('G', "Go to bottom", func() {
s.table.Bottom()
s.renderTable()
}).
Set('?', "Toggle cheatsheet", func() {
help(s.tableView)
})
Expand Down
2 changes: 1 addition & 1 deletion pkg/core/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func (a *Api) DeleteTopic(t *Topic) error {
a.KillSession(w)

// remove straight from container because parent dir has been deleted.
a.workspaces.container.Remove(w)
a.workspaces.container.Remove(w.ShortPath())
}
return nil
}
Expand Down
24 changes: 15 additions & 9 deletions pkg/core/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,33 @@ import (
)

type Container[T comparable] struct {
container map[*T]struct{}
container map[string]*T
mu *sync.RWMutex
}

func newContainer[T comparable]() *Container[T] {
return &Container[T]{
container: make(map[*T]struct{}),
container: make(map[string]*T),
mu: &sync.RWMutex{},
}
}

func (c *Container[T]) Add(d *T) {
func (c *Container[T]) Set(key string, d *T) {
c.mu.Lock()
defer c.mu.Unlock()
c.container[d] = struct{}{}
c.container[key] = d
}

func (c *Container[T]) Remove(d *T) {
func (c *Container[T]) Get(key string) *T {
c.mu.RLock()
defer c.mu.RUnlock()
return c.container[key]
}

func (c *Container[T]) Remove(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.container, d)
delete(c.container, key)
}

func (c *Container[T]) Size() int {
Expand All @@ -38,13 +44,13 @@ func (c *Container[T]) All() []*T {
c.mu.RLock()
defer c.mu.RUnlock()
out := make([]*T, 0)
for d := range c.container {
for _, d := range c.container {
out = append(out, d)
}
return out
}

func (c *Container[T]) Contains(d *T) bool {
_, exists := c.container[d]
func (c *Container[T]) Contains(key string) bool {
_, exists := c.container[key]
return exists
}
15 changes: 5 additions & 10 deletions pkg/core/topic.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,13 @@ func (tr *TopicRepository) load(rootPath string) {

topicName := topicDirEntry.Name()
topic := newTopic(topicName, filepath.Join(rootPath, topicName))
tc.Add(topic)
tc.Set(topic.Name, topic)
}
}

func (tr *TopicRepository) Save(t *Topic) error {
// if this topic doesnt exist, we create a dir
if !tr.container.Contains(t) {
if !tr.container.Contains(t.Name) {
if err := system.CreateDir(t.path); err != nil {
return err
}
Expand All @@ -114,7 +114,7 @@ func (tr *TopicRepository) Save(t *Topic) error {
}

// save it to the container
tr.container.Add(t)
tr.container.Set(t.Name, t)
return nil
}

Expand All @@ -123,17 +123,12 @@ func (tr *TopicRepository) Delete(t *Topic) error {
return err
}

tr.container.Remove(t)
tr.container.Remove(t.Name)
return nil
}

func (tr *TopicRepository) FindByName(name string) *Topic {
for _, t := range tr.container.All() {
if t.Name == name {
return t
}
}
return nil
return tr.container.Get(name)
}

func (tr *TopicRepository) All() Topics {
Expand Down
24 changes: 5 additions & 19 deletions pkg/core/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,14 @@ func (w *WorkspaceRepository) load(topics Topics) {
}

workspace := newWorkspace(topic, dirEntry.Name())
wc.Add(workspace)
wc.Set(workspace.ShortPath(), workspace)
}
}
}

func (w *WorkspaceRepository) Save(workspace *Workspace) error {
// if this workspace doesnt exist, we create a dir
if !w.container.Contains(workspace) {
if !w.container.Contains(workspace.ShortPath()) {
if err := system.CreateDir(workspace.Path()); err != nil {
return err
}
Expand All @@ -186,7 +186,7 @@ func (w *WorkspaceRepository) Save(workspace *Workspace) error {
}

// save it to the container
w.container.Add(workspace)
w.container.Set(workspace.ShortPath(), workspace)
return nil
}

Expand All @@ -195,26 +195,12 @@ func (w *WorkspaceRepository) Delete(workspace *Workspace) error {
return err
}

w.container.Remove(workspace)
w.container.Remove(workspace.ShortPath())
return nil
}

func (w *WorkspaceRepository) FindByShortPath(shortPath string) *Workspace {
for _, w := range w.container.All() {
if w.ShortPath() == shortPath {
return w
}
}
return nil
}

func (w *WorkspaceRepository) FindByPath(path string) *Workspace {
for _, w := range w.container.All() {
if w.Path() == path {
return w
}
}
return nil
return w.container.Get(shortPath)
}

func (w *WorkspaceRepository) All() Workspaces {
Expand Down
24 changes: 24 additions & 0 deletions pkg/system/fzf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package system

import (
"os/exec"
"strings"
)

func IsFzfInstalled() bool {
_, err := exec.LookPath("fzf")
return err == nil
}

func FuzzyFind(lst []string, search string) []string {
input := strings.Join(lst, "\n")
cmd := exec.Command("fzf", "--filter", search)
cmd.Stdin = strings.NewReader(input)
out, err := cmd.Output()
if err != nil {
return []string{}
}

items := strings.Split(string(out), "\n")
return items
}
Loading

0 comments on commit ff8918a

Please sign in to comment.