Move desktop-related code to external package
parent
0103b3e242
commit
f6204aca7f
|
@ -0,0 +1,108 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/jroimartin/gocui"
|
||||
)
|
||||
|
||||
func initGUI() (*gocui.Gui, error) {
|
||||
g, err := gocui.NewGui(gocui.OutputNormal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g.InputEsc = true
|
||||
g.Cursor = true
|
||||
g.Mouse = !disableMouseSupport
|
||||
|
||||
g.SetManagerFunc(layout)
|
||||
|
||||
if err := keybindings(g); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go updateEntryInfo()
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func layout(g *gocui.Gui) error {
|
||||
maxX, maxY := g.Size()
|
||||
listWidth := maxX
|
||||
if !hideAppDetails {
|
||||
listWidth = maxX / 2
|
||||
|
||||
if v, err := g.SetView("ex", maxX/2, 0, maxX, 2); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
|
||||
ex = v
|
||||
|
||||
v.Frame = false
|
||||
v.Wrap = false
|
||||
}
|
||||
if v, err := g.SetView("comment", maxX/2, 1, maxX, 3); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
|
||||
comment = v
|
||||
|
||||
v.Frame = false
|
||||
v.Wrap = false
|
||||
}
|
||||
}
|
||||
if v, err := g.SetView("list", -1, 0, listWidth, maxY); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
|
||||
list = v
|
||||
|
||||
v.Frame = false
|
||||
v.Wrap = false
|
||||
v.Highlight = true
|
||||
v.SelBgColor = gocui.ColorGreen
|
||||
v.SelFgColor = gocui.ColorBlack
|
||||
|
||||
updateEntries("")
|
||||
}
|
||||
if v, err := g.SetView("main", -1, -1, maxX, 1); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
|
||||
input = v
|
||||
|
||||
v.Frame = false
|
||||
v.Wrap = false
|
||||
v.Editable = true
|
||||
v.Wrap = true
|
||||
v.Editor = gocui.EditorFunc(searchEditor)
|
||||
|
||||
if _, err := g.SetCurrentView("main"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = maxX, maxY
|
||||
return nil
|
||||
}
|
||||
|
||||
func closeGUI() {
|
||||
if closedGUI {
|
||||
return
|
||||
}
|
||||
closedGUI = true
|
||||
|
||||
gui.Close()
|
||||
|
||||
gui.Update(func(g *gocui.Gui) error {
|
||||
return gocui.ErrQuit
|
||||
})
|
||||
}
|
||||
|
||||
func quit(g *gocui.Gui, v *gocui.View) error {
|
||||
closeGUI()
|
||||
return gocui.ErrQuit
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/jroimartin/gocui"
|
||||
)
|
||||
|
||||
func searchEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
|
||||
switch {
|
||||
case ch != 0 && mod == 0:
|
||||
v.EditWrite(ch)
|
||||
case key == gocui.KeySpace:
|
||||
v.EditWrite(' ')
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
case key == gocui.KeyDelete:
|
||||
v.EditDelete(false)
|
||||
case key == gocui.KeyInsert:
|
||||
v.Overwrite = !v.Overwrite
|
||||
case key == gocui.KeyEnter:
|
||||
err := listSelect()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return
|
||||
case key == gocui.KeyArrowLeft:
|
||||
v.MoveCursor(-1, 0, false)
|
||||
case key == gocui.KeyArrowRight:
|
||||
v.MoveCursor(1, 0, false)
|
||||
}
|
||||
|
||||
updateEntries(v.Buffer())
|
||||
}
|
||||
|
||||
func listPrev(g *gocui.Gui, v *gocui.View) error {
|
||||
list.MoveCursor(0, -1, false)
|
||||
updateEntryInfo()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listNext(g *gocui.Gui, v *gocui.View) error {
|
||||
list.MoveCursor(0, 1, false)
|
||||
updateEntryInfo()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listClickFromMouse(g *gocui.Gui, v *gocui.View) error {
|
||||
clickedList = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listSelectFromMouse(g *gocui.Gui, v *gocui.View) error {
|
||||
if !clickedList {
|
||||
return nil
|
||||
}
|
||||
|
||||
return listSelect()
|
||||
}
|
||||
|
||||
func listDeselectFromMouse(g *gocui.Gui, v *gocui.View) error {
|
||||
clickedList = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listSelectFromKey(g *gocui.Gui, v *gocui.View) error {
|
||||
return listSelect()
|
||||
}
|
||||
|
||||
func keybindings(g *gocui.Gui) error {
|
||||
if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, listPrev); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, listNext); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("", gocui.KeyEnter, gocui.ModNone, listSelectFromKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("list", gocui.MouseLeft, gocui.ModNone, listClickFromMouse); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("list", gocui.MouseRelease, gocui.ModNone, listSelectFromMouse); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("", gocui.MouseRelease, gocui.ModNone, listDeselectFromMouse); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("", gocui.KeyEsc, gocui.ModNone, quit); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~tslocum/desktop"
|
||||
"git.sr.ht/~tslocum/gmenu/pkg/config"
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func updateEntries(buf string) {
|
||||
filteredEntries = nil
|
||||
|
||||
buf = strings.ToLower(strings.TrimSpace(buf))
|
||||
if buf == "" {
|
||||
for _, entries := range desktopEntries {
|
||||
filteredEntries = append(filteredEntries, entries...)
|
||||
}
|
||||
} else {
|
||||
var ranks []int
|
||||
var rank int
|
||||
var i int
|
||||
for _, entries := range desktopEntries {
|
||||
for _, entry := range entries {
|
||||
rank = -1
|
||||
if strings.Contains(strings.ToLower(entry.Name), buf) {
|
||||
rank = 1
|
||||
}
|
||||
|
||||
if rank == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
filteredEntries = append(filteredEntries, entry)
|
||||
ranks = append(ranks, rank)
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(filteredEntries, func(i, j int) bool {
|
||||
return ranks[i] < ranks[j]
|
||||
})
|
||||
}
|
||||
|
||||
list.Clear()
|
||||
list.SetOrigin(0, 0)
|
||||
list.SetCursor(0, 0)
|
||||
defer updateEntryInfo()
|
||||
|
||||
if len(filteredEntries) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
lastEntry := len(filteredEntries) - 1
|
||||
for i, entry := range filteredEntries {
|
||||
if i == lastEntry {
|
||||
fmt.Fprint(list, entry.Name)
|
||||
} else {
|
||||
fmt.Fprint(list, entry.Name+"\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func selectedEntry() *desktop.Entry {
|
||||
if list == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, selectedOrigin := list.Origin()
|
||||
_, selectedCursor := list.Cursor()
|
||||
selected := selectedOrigin + selectedCursor
|
||||
if len(filteredEntries) == 0 || selected < 0 || selected > len(filteredEntries)-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return filteredEntries[selected]
|
||||
}
|
||||
|
||||
func updateEntryInfo() {
|
||||
if hideAppDetails {
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
if list != nil && comment != nil && ex != nil {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
var comLine, exLine string
|
||||
entry := selectedEntry()
|
||||
if entry != nil {
|
||||
comLine = entry.Comment
|
||||
exLine = entry.Exec
|
||||
} else {
|
||||
comLine = "Shell command"
|
||||
exLine = "/bin/bash"
|
||||
}
|
||||
|
||||
ex.Clear()
|
||||
fmt.Fprint(ex, exLine)
|
||||
comment.Clear()
|
||||
fmt.Fprint(comment, comLine)
|
||||
}
|
||||
|
||||
func listSelect() error {
|
||||
defer closeGUI()
|
||||
|
||||
var (
|
||||
execute string
|
||||
runInTerminal bool
|
||||
waitUntilFinished bool
|
||||
)
|
||||
entry := selectedEntry()
|
||||
if entry != nil {
|
||||
runInTerminal = entry.Terminal
|
||||
execute = entry.ExpandExec(shellquote.Join(flag.Args()...))
|
||||
} else {
|
||||
waitUntilFinished = true
|
||||
execute = input.Buffer()
|
||||
}
|
||||
execute = strings.TrimSpace(execute)
|
||||
|
||||
closeGUI()
|
||||
|
||||
log.Println(execute)
|
||||
|
||||
runScript, err := desktop.RunScript(execute)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create run script")
|
||||
}
|
||||
|
||||
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.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 gocui.ErrQuit
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
_, isExitErr := err.(*exec.ExitError)
|
||||
if err != nil && !isExitErr {
|
||||
return errors.Wrap(err, "failed to execute command")
|
||||
}
|
||||
|
||||
return gocui.ErrQuit
|
||||
}
|
|
@ -5,29 +5,22 @@ package main
|
|||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~tslocum/gmenu/pkg/config"
|
||||
"git.sr.ht/~tslocum/gmenu/pkg/dmenu"
|
||||
"git.sr.ht/~tslocum/desktop"
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
var (
|
||||
configPath string
|
||||
dataDirs string
|
||||
disableMouseSupport bool
|
||||
hideAppDetails bool
|
||||
|
||||
desktopEntries []*dmenu.DesktopEntry
|
||||
filteredEntries []*dmenu.DesktopEntry
|
||||
desktopEntries map[int][]*desktop.Entry
|
||||
filteredEntries []*desktop.Entry
|
||||
|
||||
gui *gocui.Gui
|
||||
input, comment, ex, list *gocui.View
|
||||
|
@ -39,6 +32,7 @@ var (
|
|||
func init() {
|
||||
log.SetFlags(0)
|
||||
|
||||
flag.StringVar(&dataDirs, "data-dirs", "", "application data directories (default: $XDG_DATA_DIRS)")
|
||||
flag.BoolVar(&disableMouseSupport, "no-mouse", false, "disable mouse support")
|
||||
flag.BoolVar(&hideAppDetails, "no-details", false, "hide application details")
|
||||
}
|
||||
|
@ -48,63 +42,30 @@ func main() {
|
|||
|
||||
tty := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
|
||||
if !tty {
|
||||
log.Fatal("failed to start gmenu: only interactive terminals are supported")
|
||||
log.Fatal("failed to start gmenu: non-interactive terminals are not supported")
|
||||
}
|
||||
|
||||
run()
|
||||
}
|
||||
|
||||
func run() {
|
||||
var dataDirs []string
|
||||
|
||||
dataDirsSetting := strings.Split(os.Getenv("XDG_DATA_DIRS"), ":")
|
||||
for _, dataDir := range dataDirsSetting {
|
||||
dataDir = strings.TrimSpace(dataDir)
|
||||
if dataDir == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
dataDirs = append(dataDirs, dataDir)
|
||||
var dirs []string
|
||||
if dataDirs != "" {
|
||||
dirs = strings.Split(dataDirs, ":")
|
||||
} else {
|
||||
dirs = desktop.DataDirs()
|
||||
}
|
||||
if len(dataDirs) == 0 {
|
||||
dataDirs = []string{"/usr/local/share", "/usr/share"}
|
||||
}
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if strings.TrimSpace(homeDir) == "" {
|
||||
homeDir = "~/"
|
||||
}
|
||||
dataHomeSetting := os.Getenv("XDG_DATA_HOME")
|
||||
if dataHomeSetting == "" {
|
||||
dataHomeSetting = path.Join(homeDir, ".local/share")
|
||||
}
|
||||
dataDirs = append(dataDirs, dataHomeSetting)
|
||||
|
||||
var err error
|
||||
desktopEntries, err = dmenu.ScanEntries(dataDirs)
|
||||
desktopEntries, err = desktop.Scan(dirs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
gui, err = gocui.NewGui(gocui.OutputNormal)
|
||||
gui, err = initGUI()
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
gui.InputEsc = true
|
||||
gui.Cursor = true
|
||||
gui.Mouse = !disableMouseSupport
|
||||
gui.SetManagerFunc(layout)
|
||||
|
||||
if err := keybindings(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
go updateEntryInfo()
|
||||
|
||||
go func() {
|
||||
if err := gui.MainLoop(); err != nil && err != gocui.ErrQuit {
|
||||
log.Panicln(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
done <- true
|
||||
|
@ -118,335 +79,3 @@ func run() {
|
|||
gui.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func layout(g *gocui.Gui) error {
|
||||
maxX, maxY := g.Size()
|
||||
listWidth := maxX
|
||||
if !hideAppDetails {
|
||||
listWidth = maxX / 2
|
||||
|
||||
if v, err := g.SetView("ex", maxX/2, 0, maxX, 2); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
|
||||
ex = v
|
||||
|
||||
v.Frame = false
|
||||
v.Wrap = false
|
||||
}
|
||||
if v, err := g.SetView("comment", maxX/2, 1, maxX, 3); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
|
||||
comment = v
|
||||
|
||||
v.Frame = false
|
||||
v.Wrap = false
|
||||
}
|
||||
}
|
||||
if v, err := g.SetView("list", -1, 0, listWidth, maxY); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
|
||||
list = v
|
||||
|
||||
v.Frame = false
|
||||
v.Wrap = false
|
||||
v.Highlight = true
|
||||
v.SelBgColor = gocui.ColorGreen
|
||||
v.SelFgColor = gocui.ColorBlack
|
||||
|
||||
updateEntries("")
|
||||
}
|
||||
if v, err := g.SetView("main", -1, -1, maxX, 1); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
|
||||
input = v
|
||||
|
||||
v.Frame = false
|
||||
v.Wrap = false
|
||||
v.Editable = true
|
||||
v.Wrap = true
|
||||
v.Editor = gocui.EditorFunc(searchEditor)
|
||||
|
||||
if _, err := g.SetCurrentView("main"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = maxX, maxY
|
||||
return nil
|
||||
}
|
||||
|
||||
func keybindings() error {
|
||||
if err := gui.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, listPrev); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, listNext); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.SetKeybinding("", gocui.KeyEnter, gocui.ModNone, listSelectFromKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.SetKeybinding("list", gocui.MouseLeft, gocui.ModNone, listClickFromMouse); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.SetKeybinding("list", gocui.MouseRelease, gocui.ModNone, listSelectFromMouse); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.SetKeybinding("", gocui.MouseRelease, gocui.ModNone, listDeselectFromMouse); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.SetKeybinding("", gocui.KeyEsc, gocui.ModNone, quit); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listPrev(g *gocui.Gui, v *gocui.View) error {
|
||||
list.MoveCursor(0, -1, false)
|
||||
updateEntryInfo()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listNext(g *gocui.Gui, v *gocui.View) error {
|
||||
list.MoveCursor(0, 1, false)
|
||||
updateEntryInfo()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateEntryInfo() {
|
||||
if hideAppDetails {
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
if list != nil && comment != nil && ex != nil {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
var comLine, exLine string
|
||||
entry := selectedEntry()
|
||||
if entry != nil {
|
||||
comLine = entry.Comment
|
||||
exLine = entry.Exec
|
||||
} else {
|
||||
comLine = "Shell command"
|
||||
exLine = "/bin/bash"
|
||||
}
|
||||
|
||||
ex.Clear()
|
||||
fmt.Fprint(ex, exLine)
|
||||
comment.Clear()
|
||||
fmt.Fprint(comment, comLine)
|
||||
}
|
||||
|
||||
func listClickFromMouse(g *gocui.Gui, v *gocui.View) error {
|
||||
clickedList = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listSelectFromMouse(g *gocui.Gui, v *gocui.View) error {
|
||||
if !clickedList {
|
||||
return nil
|
||||
}
|
||||
|
||||
return listSelect()
|
||||
}
|
||||
|
||||
func listDeselectFromMouse(g *gocui.Gui, v *gocui.View) error {
|
||||
clickedList = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listSelectFromKey(g *gocui.Gui, v *gocui.View) error {
|
||||
return listSelect()
|
||||
}
|
||||
|
||||
func listSelect() error {
|
||||
defer closeGUI()
|
||||
|
||||
var (
|
||||
execute string
|
||||
runInTerminal bool
|
||||
waitUntilFinished bool
|
||||
)
|
||||
entry := selectedEntry()
|
||||
if entry != nil {
|
||||
runInTerminal = entry.Terminal
|
||||
execute = dmenu.ExpandFieldCodes(entry.Exec)
|
||||
} else {
|
||||
waitUntilFinished = true
|
||||
execute = input.Buffer()
|
||||
}
|
||||
execute = strings.TrimSpace(execute)
|
||||
|
||||
closeGUI()
|
||||
|
||||
log.Println(execute)
|
||||
|
||||
runScript, err := dmenu.WriteRunScript(execute)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to write run script: %s", err)
|
||||
}
|
||||
|
||||
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.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !waitUntilFinished {
|
||||
return gocui.ErrQuit
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return gocui.ErrQuit
|
||||
}
|
||||
|
||||
func quit(g *gocui.Gui, v *gocui.View) error {
|
||||
closeGUI()
|
||||
return gocui.ErrQuit
|
||||
}
|
||||
|
||||
func selectedEntry() *dmenu.DesktopEntry {
|
||||
if list == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, selectedOrigin := list.Origin()
|
||||
_, selectedCursor := list.Cursor()
|
||||
selected := selectedOrigin + selectedCursor
|
||||
if len(filteredEntries) == 0 || selected < 0 || selected > len(desktopEntries)-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return filteredEntries[selected]
|
||||
}
|
||||
|
||||
func closeGUI() {
|
||||
if closedGUI {
|
||||
return
|
||||
}
|
||||
closedGUI = true
|
||||
|
||||
gui.Close()
|
||||
|
||||
gui.Update(func(g *gocui.Gui) error {
|
||||
return gocui.ErrQuit
|
||||
})
|
||||
}
|
||||
|
||||
func searchEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
|
||||
switch {
|
||||
case ch != 0 && mod == 0:
|
||||
v.EditWrite(ch)
|
||||
case key == gocui.KeySpace:
|
||||
v.EditWrite(' ')
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
case key == gocui.KeyDelete:
|
||||
v.EditDelete(false)
|
||||
case key == gocui.KeyInsert:
|
||||
v.Overwrite = !v.Overwrite
|
||||
case key == gocui.KeyEnter:
|
||||
listSelect()
|
||||
return
|
||||
case key == gocui.KeyArrowLeft:
|
||||
v.MoveCursor(-1, 0, false)
|
||||
case key == gocui.KeyArrowRight:
|
||||
v.MoveCursor(1, 0, false)
|
||||
}
|
||||
|
||||
updateEntries(v.Buffer())
|
||||
}
|
||||
|
||||
func updateEntries(buf string) {
|
||||
buf = strings.ToLower(strings.TrimSpace(buf))
|
||||
|
||||
if buf == "" {
|
||||
filteredEntries = desktopEntries
|
||||
} else {
|
||||
filteredEntries = nil
|
||||
|
||||
ranks := make([]int, len(desktopEntries))
|
||||
var rank int
|
||||
var i int
|
||||
for _, entry := range desktopEntries {
|
||||
rank = -1
|
||||
if strings.Contains(strings.ToLower(entry.Name), buf) {
|
||||
rank = 1
|
||||
}
|
||||
|
||||
if rank == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
filteredEntries = append(filteredEntries, entry)
|
||||
ranks[i] = rank
|
||||
i++
|
||||
}
|
||||
|
||||
sort.Slice(filteredEntries, func(i, j int) bool {
|
||||
return ranks[i] < ranks[j]
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
list.Clear()
|
||||
list.SetOrigin(0, 0)
|
||||
list.SetCursor(0, 0)
|
||||
defer updateEntryInfo()
|
||||
|
||||
if len(filteredEntries) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
lastEntry := len(filteredEntries) - 1
|
||||
for i, entry := range filteredEntries {
|
||||
if i == lastEntry {
|
||||
fmt.Fprint(list, entry.Name)
|
||||
} else {
|
||||
fmt.Fprint(list, entry.Name+"\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
3
go.mod
3
go.mod
|
@ -3,8 +3,11 @@ module git.sr.ht/~tslocum/gmenu
|
|||
go 1.12
|
||||
|
||||
require (
|
||||
git.sr.ht/~tslocum/desktop v0.1.0
|
||||
github.com/jroimartin/gocui v0.4.0
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mattn/go-isatty v0.0.8
|
||||
github.com/mattn/go-runewidth v0.0.4 // indirect
|
||||
github.com/nsf/termbox-go v0.0.0-20190624072549-eeb6cd0a1762 // indirect
|
||||
github.com/pkg/errors v0.8.1
|
||||
)
|
||||
|
|
6
go.sum
6
go.sum
|
@ -1,10 +1,16 @@
|
|||
git.sr.ht/~tslocum/desktop v0.1.0 h1:+zF9xEJMkmgRnKWxqDab+QkebPG7Hi83PhGXMQQUvKE=
|
||||
git.sr.ht/~tslocum/desktop v0.1.0/go.mod h1:cUn0Q8ALjkAq40qSei795yN3CfO5pkeYKo2gmzaZ2SI=
|
||||
github.com/jroimartin/gocui v0.4.0 h1:52jnalstgmc25FmtGcWqa0tcbMEWS6RpFLsOIO+I+E8=
|
||||
github.com/jroimartin/gocui v0.4.0/go.mod h1:7i7bbj99OgFHzo7kB2zPb8pXLqMBSQegY7azfqXMkyY=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/nsf/termbox-go v0.0.0-20190624072549-eeb6cd0a1762 h1:44Lv0bNi88GweB54TCjB/lEJgp+2Ze5WFpwNu0nh0ag=
|
||||
github.com/nsf/termbox-go v0.0.0-20190624072549-eeb6cd0a1762/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
|
@ -1,167 +0,0 @@
|
|||
package dmenu
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
scannedEntries []*DesktopEntry
|
||||
scannedBytes []byte
|
||||
scannedBytesLen int
|
||||
|
||||
entryHeader = []byte("[desktop entry]")
|
||||
entryName = []byte("name=")
|
||||
entryGenericName = []byte("genericname=")
|
||||
entryComment = []byte("comment=")
|
||||
entryIcon = []byte("icon=")
|
||||
entryExec = []byte("exec=")
|
||||
entryTerminal = []byte("terminal=true")
|
||||
entryNoDisplay = []byte("nodisplay=true")
|
||||
)
|
||||
|
||||
var quotes = map[string]string{
|
||||
`%%`: `%`,
|
||||
`\\\\ `: `\\ `,
|
||||
`\\\\` + "`": `\\` + "`",
|
||||
`\\\\$`: `\\$`,
|
||||
`\\\\(`: `\\(`,
|
||||
`\\\\)`: `\\)`,
|
||||
`\\\\\`: `\\\`,
|
||||
`\\\\\\\\`: `\\\\`,
|
||||
}
|
||||
|
||||
type DesktopEntry struct {
|
||||
Name string
|
||||
GenericName string
|
||||
Comment string
|
||||
Icon string
|
||||
|
||||
Exec string
|
||||
Terminal bool
|
||||
}
|
||||
|
||||
func (e *DesktopEntry) String() string {
|
||||
name := ""
|
||||
if e.Name != "" {
|
||||
name = e.Name
|
||||
}
|
||||
if e.GenericName != "" {
|
||||
if name != "" {
|
||||
name += " / "
|
||||
}
|
||||
name += e.GenericName
|
||||
}
|
||||
|
||||
comment := "no comment"
|
||||
if e.Comment != "" {
|
||||
comment = e.Comment
|
||||
}
|
||||
|
||||
return fmt.Sprintf("{%s - %s}", name, comment)
|
||||
}
|
||||
|
||||
func ScanEntries(dirs []string) ([]*DesktopEntry, error) {
|
||||
var entries []*DesktopEntry
|
||||
for _, dir := range dirs {
|
||||
err := filepath.Walk(filepath.Join(dir, "applications"), scanDirectory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries = append(entries, scannedEntries...)
|
||||
scannedEntries = nil
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func UnquoteExec(ex string) string {
|
||||
for qs, qr := range quotes {
|
||||
ex = strings.ReplaceAll(ex, qs, qr)
|
||||
}
|
||||
|
||||
return ex
|
||||
}
|
||||
|
||||
func ExpandFieldCodes(ex string) string {
|
||||
ex = strings.ReplaceAll(ex, "%F", "")
|
||||
ex = strings.ReplaceAll(ex, "%f", "")
|
||||
ex = strings.ReplaceAll(ex, "%U", "")
|
||||
ex = strings.ReplaceAll(ex, "%u", "")
|
||||
|
||||
return ex
|
||||
}
|
||||
|
||||
func scanDirectory(path string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f == nil || f.IsDir() || !strings.HasSuffix(strings.ToLower(path), ".desktop") {
|
||||
return nil
|
||||
}
|
||||
|
||||
entry := &DesktopEntry{}
|
||||
|
||||
file, err := os.OpenFile(path, os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
var foundHeader bool
|
||||
|
||||
for scanner.Scan() {
|
||||
scannedBytes = bytes.TrimSpace(scanner.Bytes())
|
||||
scannedBytesLen = len(scannedBytes)
|
||||
|
||||
if scannedBytesLen == 0 || scannedBytes[0] == byte('#') {
|
||||
continue
|
||||
} else if scannedBytes[0] == byte('[') {
|
||||
if !foundHeader {
|
||||
if scannedBytesLen < 15 || !bytes.EqualFold(scannedBytes[0:15], entryHeader) {
|
||||
log.Printf("Warning: invalid desktop entry %s", path)
|
||||
return nil
|
||||
} else {
|
||||
foundHeader = true
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
break // Start of new section
|
||||
}
|
||||
} else if scannedBytesLen >= 5 && bytes.EqualFold(scannedBytes[0:5], entryName) {
|
||||
entry.Name = string(scannedBytes[5:])
|
||||
} else if scannedBytesLen >= 12 && bytes.EqualFold(scannedBytes[0:12], entryGenericName) {
|
||||
entry.GenericName = string(scannedBytes[12:])
|
||||
} else if scannedBytesLen >= 8 && bytes.EqualFold(scannedBytes[0:8], entryComment) {
|
||||
entry.Comment = string(scannedBytes[8:])
|
||||
} else if scannedBytesLen >= 5 && bytes.EqualFold(scannedBytes[0:5], entryIcon) {
|
||||
entry.Icon = string(scannedBytes[5:])
|
||||
} else if scannedBytesLen >= 5 && bytes.EqualFold(scannedBytes[0:5], entryExec) {
|
||||
entry.Exec = UnquoteExec(string(scannedBytes[5:]))
|
||||
} else if scannedBytesLen >= 13 && bytes.EqualFold(scannedBytes, entryTerminal) {
|
||||
entry.Terminal = true
|
||||
} else if scannedBytesLen >= 14 && bytes.EqualFold(scannedBytes, entryNoDisplay) {
|
||||
return nil // NoDisplay=true
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if entry.Name == "" && entry.GenericName != "" {
|
||||
entry.Name = entry.GenericName
|
||||
entry.GenericName = ""
|
||||
} else if entry.Name == entry.GenericName {
|
||||
entry.GenericName = ""
|
||||
}
|
||||
|
||||
scannedEntries = append(scannedEntries, entry)
|
||||
return nil
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package dmenu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
func WriteRunScript(ex string) (string, error) {
|
||||
runScript, err := ioutil.TempFile("", "gmenu-*")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = runScript.WriteString("#!/bin/sh\n")
|
||||
if err != nil {
|
||||
runScript.Close()
|
||||
return "", err
|
||||
}
|
||||
_, err = runScript.WriteString(fmt.Sprintf("rm %s\n", runScript.Name()))
|
||||
if err != nil {
|
||||
runScript.Close()
|
||||
return "", err
|
||||
}
|
||||
_, err = runScript.WriteString("exec " + ex + "\n")
|
||||
if err != nil {
|
||||
runScript.Close()
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = runScript.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = os.Chmod(runScript.Name(), 0744)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return runScript.Name(), nil
|
||||
}
|
Loading…
Reference in New Issue