Implementing Simple Buffer Searching

Searching the Global Database although is enough but I have felt a need to have a
quick and fast search option to search the current buffer.

Buffer Search is also one of the views It can be only turned on if the File Browser
has focus. ( Thinking of making it global ). The Searching is done
through the fuzzy module. FileNode now implements the Source Interface.
The Changed Function of the Search Bar checks for text changes and then
modifies the Matches Variable which is used by the Update Function to
Draw the Results. The Results have the Matching Characters Highlighted
Differently. Maximum of 15 results are displayed to avoid lag. Upon
Selecting the Result through the Search Bar navigation is possible and
selection of the item is done the same way it works for file Browser.
After Selection the Focus is returned Back to the File Browser. For The
Tracks only the title is used for searching.
This commit is contained in:
aditya-K2 2021-12-26 00:18:23 +05:30
parent 659becf3fb
commit 175b694a4d
6 changed files with 249 additions and 113 deletions

View File

@ -7,9 +7,6 @@ import (
"github.com/fhs/gompd/mpd" "github.com/fhs/gompd/mpd"
"strings" "strings"
"github.com/aditya-K2/gomp/utils"
"github.com/aditya-K2/tview"
) )
var ( var (
@ -39,22 +36,6 @@ func TogglePlayBack() error {
return err return err
} }
func UpdatePlaylist(inputTable *tview.Table) {
_playlistAttr, _ := CONN.PlaylistInfo(-1, -1)
inputTable.Clear()
for i, j := range _playlistAttr {
_, _, w, _ := inputTable.GetInnerRect()
if j["Title"] == "" || j["Artist"] == "" || j["Album"] == "" {
inputTable.SetCell(i, 0, tview.NewTableCell(utils.GetFormattedString(j["file"], w/3)))
} else {
inputTable.SetCell(i, 0, tview.NewTableCell(utils.GetFormattedString("[green]"+j["Title"], w/3)))
inputTable.SetCell(i, 1, tview.NewTableCell(utils.GetFormattedString("[magenta]"+j["Artist"], w/3)))
inputTable.SetCell(i, 2, tview.NewTableCell("[yellow]"+j["Album"]))
}
}
}
// The GenerateContentSlice returns a slice of the content to be displayed on the Search View. The Slice is generated // The GenerateContentSlice returns a slice of the content to be displayed on the Search View. The Slice is generated
// because the random nature of maps as they return values randomly hence the draw function keeps changing the order // because the random nature of maps as they return values randomly hence the draw function keeps changing the order
// in which the results appear. // in which the results appear.
@ -99,70 +80,6 @@ func GenerateContentSlice(selectedSuggestion string) ([]interface{}, error) {
return ContentSlice, nil return ContentSlice, nil
} }
// UpdateSearchView as the name suggests Updates the Search View the idea is to basically keep a fourth option called
// Search in the Navigation bar which will render things from a global ContentSlice at least in the context of the main
// function this will also help in persisting the Search Results.
func UpdateSearchView(inputTable *tview.Table, c []interface{}) {
inputTable.Clear()
_, _, width, _ := inputTable.GetInnerRect()
for i, content := range c {
switch content.(type) {
case [3]string:
{
inputTable.SetCell(i, 0, tview.NewTableCell(utils.GetFormattedString("[green]"+content.([3]string)[0], width/3)))
inputTable.SetCell(i, 1, tview.NewTableCell(utils.GetFormattedString("[magenta]"+content.([3]string)[1], width/3)))
inputTable.SetCell(i, 2, tview.NewTableCell(utils.GetFormattedString("[yellow]"+content.([3]string)[2], width/3)))
}
case [2]string:
{
inputTable.SetCell(i, 0, tview.NewTableCell(utils.GetFormattedString("[green]"+content.([2]string)[0], width/3)))
inputTable.SetCell(i, 1, tview.NewTableCell(utils.GetFormattedString("[magenta]"+content.([2]string)[1], width/3)))
}
case string:
{
b := content.(string)
if !strings.HasPrefix(b, WHITE_AND_BOLD) {
inputTable.SetCell(i, 0, tview.NewTableCell("[green]"+content.(string)))
} else {
inputTable.SetCell(i, 0, tview.NewTableCell(content.(string)).SetSelectable(false))
}
}
}
}
}
func Update(f []FileNode, inputTable *tview.Table) {
inputTable.Clear()
for i, j := range f {
if len(j.Children) == 0 {
_songAttributes, err := CONN.ListAllInfo(j.AbsolutePath)
if err == nil && _songAttributes[0]["Title"] != "" {
_, _, w, _ := inputTable.GetInnerRect()
inputTable.SetCell(i, 0,
tview.NewTableCell("[green]"+utils.GetFormattedString(_songAttributes[0]["Title"], w/3)).
SetAlign(tview.AlignLeft))
inputTable.SetCell(i, 1,
tview.NewTableCell("[magenta]"+utils.GetFormattedString(_songAttributes[0]["Artist"], w/3)).
SetAlign(tview.AlignLeft))
inputTable.SetCell(i, 2,
tview.NewTableCell("[yellow]"+_songAttributes[0]["Album"]).
SetAlign(tview.AlignLeft))
} else if _songAttributes[0]["Title"] == "" {
inputTable.SetCell(i, 0,
tview.NewTableCell("[blue]"+j.Path).
SetAlign(tview.AlignLeft))
}
} else {
inputTable.SetCell(i, 0,
tview.NewTableCell("[yellow::b]"+j.Path).
SetAlign(tview.AlignLeft))
}
}
}
// GenerateArtistTree Artist Tree is a map of Artist to their Album Map // GenerateArtistTree Artist Tree is a map of Artist to their Album Map
// Album Tree is a map of the tracks in that particular album. // Album Tree is a map of the tracks in that particular album.
func GenerateArtistTree() (map[string]map[string]map[string]string, error) { func GenerateArtistTree() (map[string]map[string]map[string]string, error) {

View File

@ -6,12 +6,31 @@ import (
) )
type FileNode struct { type FileNode struct {
Children []FileNode Children []FileNode
Path string Path string
Parent *FileNode Parent *FileNode
AbsolutePath string AbsolutePath string
} }
// Source Interface For Fuzzy Searching.
type FileNodes []FileNode
func (f FileNodes) String(i int) string {
if len(f[i].Children) == 0 {
_s, err := CONN.ListAllInfo(f[i].AbsolutePath)
if err != nil {
NotificationServer.Send(fmt.Sprintf("Could Not Get Information About the Node %s", f[i].Path))
return f[i].Path
}
return _s[0]["Title"]
}
return f[i].Path
}
func (f FileNodes) Len() int {
return len(f)
}
func (f *FileNode) AddChildren(path string) { func (f *FileNode) AddChildren(path string) {
if f.Path != "" { if f.Path != "" {
f.Children = append(f.Children, FileNode{Children: make([]FileNode, 0), Path: path, Parent: f, AbsolutePath: f.AbsolutePath + "/" + path}) f.Children = append(f.Children, FileNode{Children: make([]FileNode, 0), Path: path, Parent: f, AbsolutePath: f.AbsolutePath + "/" + path})

113
client/updateView.go Normal file
View File

@ -0,0 +1,113 @@
package client
import (
"fmt"
"strings"
"github.com/aditya-K2/fuzzy"
"github.com/aditya-K2/gomp/utils"
"github.com/aditya-K2/tview"
)
func UpdateBuffSearchView(inputTable *tview.Table, m fuzzy.Matches, f []FileNode) {
inputTable.Clear()
if m == nil || len(m) == 0 {
Update(f, inputTable)
} else {
for k, v := range m {
if len(f[v.Index].Children) != 0 {
inputTable.SetCellSimple(k, 0, utils.GetMatchedString(f[v.Index].Path, "#0000ff", "yellow", v.MatchedIndexes))
} else {
_s, err := CONN.ListAllInfo(f[v.Index].AbsolutePath)
if err != nil {
NotificationServer.Send(fmt.Sprintf("Could Not Add %s to the Table", f[v.Index].Path))
} else {
inputTable.SetCellSimple(k, 0, utils.GetMatchedString(_s[0]["Title"], "#fbff00", "green", v.MatchedIndexes))
}
}
if k == 15 {
break
}
}
}
}
func UpdatePlaylist(inputTable *tview.Table) {
_playlistAttr, _ := CONN.PlaylistInfo(-1, -1)
inputTable.Clear()
for i, j := range _playlistAttr {
_, _, w, _ := inputTable.GetInnerRect()
if j["Title"] == "" || j["Artist"] == "" || j["Album"] == "" {
inputTable.SetCell(i, 0, tview.NewTableCell(utils.GetFormattedString(j["file"], w/3)))
} else {
inputTable.SetCell(i, 0, tview.NewTableCell(utils.GetFormattedString("[green]"+j["Title"], w/3)))
inputTable.SetCell(i, 1, tview.NewTableCell(utils.GetFormattedString("[magenta]"+j["Artist"], w/3)))
inputTable.SetCell(i, 2, tview.NewTableCell("[yellow]"+j["Album"]))
}
}
}
// UpdateSearchView as the name suggests Updates the Search View the idea is to basically keep a fourth option called
// Search in the Navigation bar which will render things from a global ContentSlice at least in the context of the main
// function this will also help in persisting the Search Results.
func UpdateSearchView(inputTable *tview.Table, c []interface{}) {
inputTable.Clear()
_, _, width, _ := inputTable.GetInnerRect()
for i, content := range c {
switch content.(type) {
case [3]string:
{
inputTable.SetCell(i, 0, tview.NewTableCell(utils.GetFormattedString("[green]"+content.([3]string)[0], width/3)))
inputTable.SetCell(i, 1, tview.NewTableCell(utils.GetFormattedString("[magenta]"+content.([3]string)[1], width/3)))
inputTable.SetCell(i, 2, tview.NewTableCell(utils.GetFormattedString("[yellow]"+content.([3]string)[2], width/3)))
}
case [2]string:
{
inputTable.SetCell(i, 0, tview.NewTableCell(utils.GetFormattedString("[green]"+content.([2]string)[0], width/3)))
inputTable.SetCell(i, 1, tview.NewTableCell(utils.GetFormattedString("[magenta]"+content.([2]string)[1], width/3)))
}
case string:
{
b := content.(string)
if !strings.HasPrefix(b, WHITE_AND_BOLD) {
inputTable.SetCell(i, 0, tview.NewTableCell("[green]"+content.(string)))
} else {
inputTable.SetCell(i, 0, tview.NewTableCell(content.(string)).SetSelectable(false))
}
}
}
}
}
func Update(f []FileNode, inputTable *tview.Table) {
inputTable.Clear()
for i, j := range f {
if len(j.Children) == 0 {
_songAttributes, err := CONN.ListAllInfo(j.AbsolutePath)
if err == nil && _songAttributes[0]["Title"] != "" {
_, _, w, _ := inputTable.GetInnerRect()
inputTable.SetCell(i, 0,
tview.NewTableCell("[green]"+utils.GetFormattedString(_songAttributes[0]["Title"], w/3)).
SetAlign(tview.AlignLeft))
inputTable.SetCell(i, 1,
tview.NewTableCell("[magenta]"+utils.GetFormattedString(_songAttributes[0]["Artist"], w/3)).
SetAlign(tview.AlignLeft))
inputTable.SetCell(i, 2,
tview.NewTableCell("[yellow]"+_songAttributes[0]["Album"]).
SetAlign(tview.AlignLeft))
} else if _songAttributes[0]["Title"] == "" {
inputTable.SetCell(i, 0,
tview.NewTableCell("[blue]"+j.Path).
SetAlign(tview.AlignLeft))
}
} else {
inputTable.SetCell(i, 0,
tview.NewTableCell("[yellow::b]"+j.Path).
SetAlign(tview.AlignLeft))
}
}
}

109
main.go
View File

@ -4,17 +4,15 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/aditya-K2/gomp/ui/notify" "github.com/aditya-K2/gomp/cache"
"github.com/aditya-K2/gomp/client"
"github.com/aditya-K2/gomp/config"
"github.com/aditya-K2/gomp/render" "github.com/aditya-K2/gomp/render"
"github.com/aditya-K2/gomp/ui" "github.com/aditya-K2/gomp/ui"
"github.com/aditya-K2/gomp/ui/notify"
"github.com/aditya-K2/gomp/client"
"github.com/aditya-K2/gomp/utils" "github.com/aditya-K2/gomp/utils"
"github.com/aditya-K2/fuzzy" "github.com/aditya-K2/fuzzy"
"github.com/aditya-K2/gomp/cache"
"github.com/aditya-K2/gomp/config"
"github.com/fhs/gompd/mpd" "github.com/fhs/gompd/mpd"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -81,6 +79,7 @@ func main() {
// This is the Slice that is used to Display Content in the SearchView // This is the Slice that is used to Display Content in the SearchView
var SearchContentSlice []interface{} var SearchContentSlice []interface{}
var Matches fuzzy.Matches
// This Function Is Responsible for Changing the Focus it uses the Focus Map and Based on it Chooses // This Function Is Responsible for Changing the Focus it uses the Focus Map and Based on it Chooses
// the Draw Function // the Draw Function
@ -91,6 +90,8 @@ func main() {
client.UpdateSearchView(UI.ExpandedView, SearchContentSlice) client.UpdateSearchView(UI.ExpandedView, SearchContentSlice)
} else if ui.HasFocus("FileBrowser") { } else if ui.HasFocus("FileBrowser") {
client.Update(dirTree.Children, UI.ExpandedView) client.Update(dirTree.Children, UI.ExpandedView)
} else if ui.HasFocus("BuffSearchView") {
client.UpdateBuffSearchView(UI.ExpandedView, Matches, dirTree.Children)
} }
return UI.ExpandedView.GetInnerRect() return UI.ExpandedView.GetInnerRect()
}) })
@ -100,20 +101,36 @@ func main() {
// the respective function in this case togglePlayBack is called. // the respective function in this case togglePlayBack is called.
var FuncMap = map[string]func(){ var FuncMap = map[string]func(){
"showChildrenContent": func() { "showChildrenContent": func() {
r, _ := UI.ExpandedView.GetSelection()
if ui.HasFocus("FileBrowser") { if ui.HasFocus("FileBrowser") {
r, _ := UI.ExpandedView.GetSelection()
ui.SetFocus("FileBrowser")
if len(dirTree.Children[r].Children) == 0 { if len(dirTree.Children[r].Children) == 0 {
id, _ := CONN.AddId(dirTree.Children[r].AbsolutePath, -1) id, _ := CONN.AddId(dirTree.Children[r].AbsolutePath, -1)
CONN.PlayId(id) CONN.PlayId(id)
} else { } else {
client.Update(dirTree.Children[r].Children, UI.ExpandedView) client.Update(dirTree.Children[r].Children, UI.ExpandedView)
dirTree = &dirTree.Children[r] dirTree = &dirTree.Children[r]
UI.ExpandedView.Select(0, 0)
} }
} else if ui.HasFocus("Playlist") { } else if ui.HasFocus("Playlist") {
r, _ := UI.ExpandedView.GetSelection()
CONN.Play(r) CONN.Play(r)
} else if ui.HasFocus("SearchView") { } else if ui.HasFocus("SearchView") {
r, _ := UI.ExpandedView.GetSelection() r, _ := UI.ExpandedView.GetSelection()
client.AddToPlaylist(SearchContentSlice[r], true) client.AddToPlaylist(SearchContentSlice[r], true)
} else if ui.HasFocus("BuffSearchView") {
r, _ := UI.ExpandedView.GetSelection()
ui.SetFocus("FileBrowser")
if len(dirTree.Children[r].Children) == 0 {
id, _ := CONN.AddId(dirTree.Children[Matches[r].Index].AbsolutePath, -1)
CONN.PlayId(id)
} else {
client.Update(dirTree.Children[Matches[r].Index].Children, UI.ExpandedView)
dirTree = &dirTree.Children[Matches[r].Index]
}
UI.SearchBar.SetText("")
// Resetting Matches
Matches = nil
} }
}, },
"togglePlayBack": func() { "togglePlayBack": func() {
@ -192,7 +209,13 @@ func main() {
UI.Navbar.Select(3, 0) UI.Navbar.Select(3, 0)
}, },
"quit": func() { "quit": func() {
UI.App.Stop() if ui.HasFocus("BuffSearchView") {
ui.SetFocus("FileBrowser")
UI.SearchBar.SetText("")
Matches = nil
} else {
UI.App.Stop()
}
}, },
"stop": func() { "stop": func() {
CONN.Stop() CONN.Stop()
@ -214,6 +237,12 @@ func main() {
"FocusSearch": func() { "FocusSearch": func() {
UI.App.SetFocus(UI.SearchBar) UI.App.SetFocus(UI.SearchBar)
}, },
"FocusBuffSearch": func() {
if ui.HasFocus("FileBrowser") || ui.HasFocus("BuffSearchView") {
ui.SetFocus("BuffSearchView")
UI.App.SetFocus(UI.SearchBar)
}
},
} }
// Generating the Key Map Based on the Function Map Here Basically the Values will be flipped // Generating the Key Map Based on the Function Map Here Basically the Values will be flipped
@ -222,19 +251,23 @@ func main() {
config.GenerateKeyMap(FuncMap) config.GenerateKeyMap(FuncMap)
UI.SearchBar.SetAutocompleteFunc(func(c string) []string { UI.SearchBar.SetAutocompleteFunc(func(c string) []string {
if c != "" && c != " " && c != " " { if ui.HasFocus("BuffSearchView") {
_, _, w, _ := UI.SearchBar.GetRect() return nil
matches := fuzzy.Find(c, ArtistTreeContent)
var suggestions []string
for i, match := range matches {
if i == 10 {
break
}
suggestions = append(suggestions, utils.GetFormattedString(match.Str, w-2))
}
return suggestions
} else { } else {
return make([]string, 0) if c != "" && c != " " && c != " " {
_, _, w, _ := UI.SearchBar.GetRect()
matches := fuzzy.Find(c, ArtistTreeContent)
var suggestions []string
for i, match := range matches {
if i == 10 {
break
}
suggestions = append(suggestions, utils.GetFormattedString(match.Str, w-2))
}
return suggestions
} else {
return make([]string, 0)
}
} }
}) })
@ -251,23 +284,41 @@ func main() {
UI.SearchBar.SetDoneFunc(func(e tcell.Key) { UI.SearchBar.SetDoneFunc(func(e tcell.Key) {
if e == tcell.KeyEnter { if e == tcell.KeyEnter {
UI.ExpandedView.Select(0, 0) UI.ExpandedView.Select(0, 0)
ui.SetFocus("SearchView") if ui.HasFocus("BuffSearchView") {
SearchContentSlice = nil
SearchContentSlice, err = client.GenerateContentSlice(UI.SearchBar.GetText())
if err != nil {
Notify.Send("Could Not Retrieve the Results")
} else {
UI.SearchBar.SetText("")
UI.App.SetFocus(UI.ExpandedView) UI.App.SetFocus(UI.ExpandedView)
UI.Navbar.Select(3, 0) } else {
ui.SetFocus("SearchView")
SearchContentSlice = nil
SearchContentSlice, err = client.GenerateContentSlice(UI.SearchBar.GetText())
if err != nil {
Notify.Send("Could Not Retrieve the Results")
} else {
UI.SearchBar.SetText("")
UI.App.SetFocus(UI.ExpandedView)
UI.Navbar.Select(3, 0)
}
} }
} }
if e == tcell.KeyEscape { if e == tcell.KeyEscape {
ui.FocusMap["SearchView"] = false if ui.HasFocus("SearchView") {
ui.FocusMap["SearchView"] = false
} else if ui.HasFocus("BuffSearchView") {
ui.SetFocus("FileBrowser")
Matches = nil
}
UI.SearchBar.SetText("")
UI.App.SetFocus(UI.ExpandedView) UI.App.SetFocus(UI.ExpandedView)
} }
}) })
UI.SearchBar.SetChangedFunc(func(text string) {
if ui.HasFocus("BuffSearchView") {
var f client.FileNodes = dirTree.Children
Matches = fuzzy.FindFrom(text, f)
client.UpdateBuffSearchView(UI.ExpandedView, Matches, dirTree.Children)
}
})
go func() { go func() {
for { for {
UI.App.Draw() UI.App.Draw()

View File

@ -10,6 +10,7 @@ func GenerateFocusMap() {
FocusMap["Playlist"] = true FocusMap["Playlist"] = true
FocusMap["FileBrowser"] = false FocusMap["FileBrowser"] = false
FocusMap["SearchView"] = false FocusMap["SearchView"] = false
FocusMap["BuffSearchView"] = false
} }
func HasFocus(s string) bool { func HasFocus(s string) bool {

View File

@ -1,6 +1,7 @@
package utils package utils
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"strconv" "strconv"
"strings" "strings"
@ -136,3 +137,37 @@ func CheckDirectoryFmt(path string) string {
return path + "/" return path + "/"
} }
} }
func GetMatchedString(s string, color string, nulcol string, matchedIndexes []int) string {
// The indexes that we will receive from the matchedIndexes are always sorted so we have to just
// add the color string at
// `indexValue + ( len(colorString) * k )`
// where k is the index of the indexValue in the matchedIndexes slice
// and we will need to also reset the colors, For that we check if the next indexValue in the matchedIndexes for
// the current indexValue is not the consecutive value ( v + 1 ) if yes ( is not consecutive ) then we add the reset
// color string at the k + 1 index in the string.
// for e.g.
// if we have the following matchedIndexes slice
// []int{ 1, 3, 4, 6}
// During the First Iteration matchedIndexes[k] = 1 and and matchedIndexes[k+1] are not consecutive so the nulcol
// string will be added to the matchedIndexes[k] + 1 index of the string
// During the Second Iteration as 3, 4 are consecutive the nulcol will be skipped.
color = fmt.Sprintf("[%s:-:bi]", color)
nulcol = fmt.Sprintf("[%s:-:b]", nulcol)
nulc := 0
for k := range matchedIndexes {
s = InsertAt(s, color, matchedIndexes[k]+(len(color)*k)+nulc)
if k < len(matchedIndexes)-1 && matchedIndexes[k]-matchedIndexes[k+1] != 1 {
s = InsertAt(s, nulcol, (matchedIndexes[k]+1)+(len(color)*(k+1))+nulc)
nulc += len(nulcol)
}
if k == len(matchedIndexes)-1 {
s = InsertAt(s, nulcol, ((matchedIndexes[len(matchedIndexes)-1] + 1) +
(len(matchedIndexes) * len(color)) +
(len(nulcol) * (len(matchedIndexes) - 1))))
}
}
// Adding the Nulcol at the Start
s = nulcol + s
return s
}