239 lines
5.8 KiB
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)
|
|
}
|