gmenu/pkg/gmenu/gmenu.go

239 lines
5.8 KiB
Go

package gmenu
import (
"flag"
"fmt"
"log"
"sort"
"strings"
"code.rocketnine.space/tslocum/desktop"
"github.com/lithammer/fuzzysearch/fuzzy"
)
var (
// Entries is a slice of all desktop entries.
Entries []*desktop.Entry
// Names is a slice of all desktop entry names.
Names []string
// FilteredEntries is a slice of filtered desktop entries.
FilteredEntries []*ListEntry
inputBuffer = make(chan string, 3)
input string
inputLower string
inputFlushed = make(chan bool)
)
// ListEntry is a desktop entry and its label.
type ListEntry struct {
*desktop.Entry
Label string
}
// InputUpdateHandler is a handler to be executed when the input is updated.
type InputUpdateHandler func(input string)
// SharedInit performs any necessary initialization shared between gmenu and gtkmenu.
func SharedInit(c *Config) {
log.SetFlags(0)
flag.BoolVar(&c.PrintVersion, "version", false, "print version information and exit")
flag.StringVar(&c.DataDirs, "data-dirs", "", "application data directories (default: $XDG_DATA_DIRS)")
flag.BoolVar(&c.HideGenericNames, "no-generic", false, "hide application generic names")
flag.BoolVar(&c.HideAppDetails, "no-details", false, "hide application details")
flag.StringVar(&c.terminalCommand, "terminal", "", "terminal command")
flag.StringVar(&c.browserCommand, "browser", "", "browser command")
}
// HandleInput is a goroutine which reads changes in the input buffer and calls the supplied InputUpdateHandler.
func HandleInput(u InputUpdateHandler) {
for in := range inputBuffer {
input = in
inputLower = strings.ToLower(in)
u(input)
}
inputFlushed <- true
}
// LoadEntries scans for and loads desktop entries.
func LoadEntries(c *Config) {
var err error
Entries, Names, err = DesktopEntries(c)
if err != nil {
log.Fatalf("failed to load desktop entries: %s", err)
}
}
// SetInput sets the input buffer.
func SetInput(i string) {
inputBuffer <- i
}
// CloseInput closes the input buffer.
func CloseInput() {
close(inputBuffer)
<-inputFlushed
}
// MatchEntry returns whether the entry at the supplied index matches the input buffer.
func MatchEntry(i int) bool {
if i == -1 {
return true
}
if inputLower == "" {
return true
}
return fuzzy.MatchFold(inputLower, Names[i])
}
// FilterEntries sets FilteredEntries to all entries matching the input buffer.
func FilterEntries() {
FilteredEntries = nil
if input == "" {
for i, l := range Names {
FilteredEntries = append(FilteredEntries, &ListEntry{Label: l, Entry: Entries[i]})
}
sort.Slice(FilteredEntries, SortEmpty)
} else {
matches := fuzzy.RankFindFold(inputLower, Names)
sort.Sort(matches)
for _, match := range matches {
FilteredEntries = append(FilteredEntries, &ListEntry{Label: Names[match.OriginalIndex], Entry: Entries[match.OriginalIndex]})
}
sort.Slice(FilteredEntries, SortFiltered)
}
}
// DesktopEntries scans for desktop entries.
func DesktopEntries(c *Config) ([]*desktop.Entry, []string, error) {
var dirs []string
if c.DataDirs != "" {
dirs = strings.Split(c.DataDirs, ":")
} else {
dirs = desktop.DataDirs()
}
allEntries, err := desktop.Scan(dirs)
if err != nil {
return nil, nil, err
}
var (
desktopEntries []*desktop.Entry
desktopNames []string
)
for _, entries := range allEntries {
for _, entry := range entries {
switch entry.Type {
case desktop.Application:
if entry.Exec == "" {
continue
}
case desktop.Link:
if entry.URL == "" {
continue
}
default:
continue // Unsupported entry type
}
if entry.Name != "" {
desktopEntries = append(desktopEntries, entry)
desktopNames = append(desktopNames, entry.Name)
}
if !c.HideGenericNames && entry.GenericName != "" {
desktopEntries = append(desktopEntries, entry)
desktopNames = append(desktopNames, entry.GenericName)
}
}
}
return desktopEntries, desktopNames, nil
}
// Sort returns whether entry i should be sorted before entry j.
func Sort(i, j int) bool {
if input == "" {
return SortEmpty(i, j)
}
return SortFiltered(i, j)
}
// SortEmpty returns whether entry i should be sorted before entry j when the
// input buffer is blank.
func SortEmpty(i, j int) bool {
ilower := strings.ToLower(FilteredEntries[i].Label)
jlower := strings.ToLower(FilteredEntries[j].Label)
if FilteredEntries[i].Entry == nil && FilteredEntries[j].Entry != nil {
return true
} else if ilower != jlower {
return ilower < jlower
} else {
return i < j
}
}
// SortFiltered returns whether entry i should be sorted before entry j when
// the input buffer is not blank.
func SortFiltered(i, j int) bool {
ilower := strings.ToLower(FilteredEntries[i].Label)
if FilteredEntries[i].Entry == nil {
ilower = ""
}
jlower := strings.ToLower(FilteredEntries[j].Label)
if FilteredEntries[j].Entry == nil {
jlower = ""
}
ipre := strings.HasPrefix(ilower, inputLower)
jpre := strings.HasPrefix(jlower, inputLower)
icon := strings.Contains(ilower, inputLower)
jcon := strings.Contains(jlower, inputLower)
imatch := fuzzy.MatchFold(inputLower, ilower)
jmatch := fuzzy.MatchFold(inputLower, jlower)
if ipre != jpre {
return ipre && !jpre
} else if icon != jcon {
return icon && !jcon
} else if imatch != jmatch {
return imatch && !jmatch
} else if (FilteredEntries[i].Entry == nil) != (FilteredEntries[j].Entry == nil) {
return FilteredEntries[i].Entry == nil
} else if ilower != jlower {
return ilower < jlower
} else {
return i < j
}
}
// Run executes the specified command.
func Run(config *Config, execute string, path string, runInTerminal bool, waitUntilFinished bool) error {
execute = strings.TrimSpace(execute)
fmt.Println(execute)
runScript, err := desktop.RunScript(execute)
if err != nil {
return fmt.Errorf("failed to create run script: %s", err)
}
return run(config, runScript, path, waitUntilFinished, runInTerminal)
}