parent
5fdce165ba
commit
1bb42b05cc
@ -1,2 +1,9 @@
|
||||
0.2.0:
|
||||
- Added GTK interface
|
||||
- Added Support for link (URL shortcut) desktop entries
|
||||
|
||||
0.1.1:
|
||||
- Added fuzzy string search
|
||||
|
||||
0.1.0:
|
||||
- Initial release
|
||||
|
@ -0,0 +1,29 @@
|
||||
package gmenu
|
||||
|
||||
type Config struct {
|
||||
DataDirs string
|
||||
|
||||
HideGenericNames bool
|
||||
HideAppDetails bool
|
||||
|
||||
terminalCommand string
|
||||
browserCommand string
|
||||
}
|
||||
|
||||
func (c *Config) TerminalCommand() string {
|
||||
if c.terminalCommand == "" {
|
||||
c.terminalCommand = "i3-sensible-terminal"
|
||||
}
|
||||
|
||||
return c.terminalCommand
|
||||
}
|
||||
|
||||
func (c *Config) BrowserCommand() string {
|
||||
if c.browserCommand == "" {
|
||||
c.browserCommand = "xdg-open"
|
||||
}
|
||||
|
||||
return c.browserCommand
|
||||
}
|
||||
|
||||
var Version string
|
@ -0,0 +1,219 @@
|
||||
package gmenu
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~tslocum/desktop"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
Entries []*desktop.Entry
|
||||
Names []string
|
||||
|
||||
FilteredEntries []*ListEntry
|
||||
|
||||
inputBuffer = make(chan string, 3)
|
||||
input string
|
||||
inputFlushed = make(chan bool)
|
||||
)
|
||||
|
||||
type ListEntry struct {
|
||||
*desktop.Entry
|
||||
|
||||
Label string
|
||||
}
|
||||
|
||||
type InputUpdateHandler func(input string)
|
||||
|
||||
func SharedInit(c *Config) {
|
||||
log.SetFlags(0)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
func HandleInput(u InputUpdateHandler) {
|
||||
var (
|
||||
in string
|
||||
ok bool
|
||||
)
|
||||
|
||||
inputLoop:
|
||||
for {
|
||||
select {
|
||||
case in, ok = <-inputBuffer:
|
||||
if !ok {
|
||||
break inputLoop
|
||||
}
|
||||
|
||||
input = in
|
||||
u(input)
|
||||
}
|
||||
}
|
||||
|
||||
inputFlushed <- true
|
||||
}
|
||||
|
||||
func LoadEntries(c *Config) {
|
||||
var err error
|
||||
Entries, Names, err = DesktopEntries(c)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func SetInput(i string) {
|
||||
inputBuffer <- i
|
||||
}
|
||||
|
||||
func CloseInput() {
|
||||
close(inputBuffer)
|
||||
<-inputFlushed
|
||||
}
|
||||
|
||||
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 {
|
||||
b := strings.ToLower(input)
|
||||
|
||||
matches := fuzzy.RankFindFold(b, Names)
|
||||
sort.Sort(matches)
|
||||
|
||||
for _, match := range matches {
|
||||
FilteredEntries = append(FilteredEntries, &ListEntry{Label: Names[match.OriginalIndex], Entry: Entries[match.OriginalIndex]})
|
||||
}
|
||||
|
||||
sort.Slice(FilteredEntries, SortFiltered)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func Sort(i, j int) bool {
|
||||
if input == "" {
|
||||
return SortEmpty(i, j)
|
||||
}
|
||||
|
||||
return SortFiltered(i, j)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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, input)
|
||||
jpre := strings.HasPrefix(jlower, input)
|
||||
|
||||
icon := strings.Contains(ilower, input)
|
||||
jcon := strings.Contains(jlower, input)
|
||||
|
||||
imatch := fuzzy.MatchFold(input, ilower)
|
||||
jmatch := fuzzy.MatchFold(input, 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
|
||||
}
|
||||
}
|
||||
|
||||
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 errors.Wrap(err, "failed to create run script")
|
||||
}
|
||||
|
||||
return run(config, runScript, path, waitUntilFinished, runInTerminal)
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package gmenu
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func run(config *Config, runScript string, path string, waitUntilFinished, runInTerminal bool) error {
|
||||
var cmd *exec.Cmd
|
||||
if runInTerminal {
|
||||
cmd = exec.Command(config.TerminalCommand(), "-e", runScript)
|
||||
} else {
|
||||
cmd = exec.Command("/usr/bin/env", "bash", "-c", runScript)
|
||||
}
|
||||
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pgid: 0}
|
||||
cmd.Dir = path
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to start command")
|
||||
}
|
||||
|
||||
if !waitUntilFinished {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
_, isExitErr := err.(*exec.ExitError)
|
||||
if err != nil && !isExitErr {
|
||||
return errors.Wrap(err, "failed to execute command")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
// +build windows
|
||||
|
||||
package gmenu
|
||||
|
||||
func run(config *Config, runScript string, path string, waitUntilFinished, runInTerminal bool) error {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue