Use gocui for UI

This commit is contained in:
Trevor Slocum 2019-07-11 18:18:54 -07:00
parent cc4942dcf7
commit 0dbc0fb4d3
5 changed files with 317 additions and 104 deletions

View File

@ -5,27 +5,44 @@ 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"
"github.com/tslocum/promptui"
"github.com/jroimartin/gocui"
"github.com/mattn/go-isatty"
)
var (
desktopEntries []*dmenu.DesktopEntry
filteredEntries []*dmenu.DesktopEntry
gui *gocui.Gui
input, comment, ex, list *gocui.View
closedGUI bool
done = make(chan bool)
)
func init() {
log.SetFlags(0)
promptui.SearchPrompt = ""
}
func main() {
flag.Parse()
tty := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
if !tty {
log.Fatal("failed to start gmenu: only interactive terminals are supported")
}
var dataDirs []string
dataDirsSetting := strings.Split(os.Getenv("XDG_DATA_DIRS"), ":")
@ -51,59 +68,194 @@ func main() {
}
dataDirs = append(dataDirs, dataHomeSetting)
desktopEntries, err := dmenu.ScanEntries(dataDirs)
var err error
desktopEntries, err = dmenu.ScanEntries(dataDirs)
if err != nil {
log.Fatal(err)
}
searcher := func(input string, index int) bool {
entry := desktopEntries[index]
name := strings.Replace(strings.ToLower(entry.Name), " ", "", -1)
genericName := strings.Replace(strings.ToLower(entry.GenericName), " ", "", -1)
input = strings.Replace(strings.ToLower(input), " ", "", -1)
return strings.Contains(name, input) || strings.Contains(genericName, input)
}
templates := &promptui.SelectTemplates{
Label: "Launch:",
Active: ">{{ .Name }}",
Inactive: " {{ .Name }}",
Selected: "Launching {{ .Name }}...",
Details: `{{ "Exec:" | faint }} {{ printf "%.60s" .Exec }}`,
Help: "",
}
prompt := promptui.Select{
Items: desktopEntries,
IsVimMode: false,
Templates: templates,
Searcher: searcher,
StartInSearchMode: true,
EnterAlwaysReturns: true,
}
selected, input, err := prompt.Run()
gui, err = gocui.NewGui(gocui.OutputNormal)
if err != nil {
log.Fatal(err)
} else if selected < 0 && strings.TrimSpace(input) == "" {
return
log.Panicln(err)
}
gui.InputEsc = true
gui.Cursor = true
gui.Mouse = true
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)
}
done <- true
}()
<-done
if !closedGUI {
closedGUI = true
gui.Close()
}
}
func layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
if v, err := g.SetView("comment", -1, 0, maxX, 2); err != nil {
if err != gocui.ErrUnknownView {
return err
}
comment = v
v.Frame = false
fmt.Fprint(v, "Comm: Test")
}
if v, err := g.SetView("ex", -1, 1, maxX, 3); err != nil {
if err != gocui.ErrUnknownView {
return err
}
ex = v
v.Frame = false
fmt.Fprint(v, "Exec: Test")
}
if v, err := g.SetView("list", -1, 2, maxX, maxY); err != nil {
if err != gocui.ErrUnknownView {
return err
}
list = v
v.Frame = false
v.Highlight = true
v.SelBgColor = gocui.ColorGreen
v.SelFgColor = gocui.ColorBlack
updateEntries("")
}
if v, err := g.SetView("main", -1, -1, 40, 1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
input = v
v.Frame = 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("", 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() {
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"
}
comment.Clear()
fmt.Fprint(comment, "Comm: "+comLine)
ex.Clear()
fmt.Fprint(ex, "Exec: "+exLine)
}
func listSelectFromKey(g *gocui.Gui, v *gocui.View) error {
return listSelect()
}
func listSelect() error {
defer closeGUI()
var (
ex string
execute string
runInTerminal bool
waitUntilFinished bool
)
if selected >= 0 {
runInTerminal = desktopEntries[selected].Terminal
ex = dmenu.ExpandFieldCodes(desktopEntries[selected].Exec)
entry := selectedEntry()
if entry != nil {
runInTerminal = entry.Terminal
execute = dmenu.ExpandFieldCodes(entry.Exec)
} else {
waitUntilFinished = true
ex = input
execute = input.Buffer()
}
execute = strings.TrimSpace(execute)
log.Println(ex)
closeGUI()
runScript, err := dmenu.WriteRunScript(ex)
log.Println(execute)
runScript, err := dmenu.WriteRunScript(execute)
if err != nil {
log.Fatalf("failed to write run script: %s", err)
}
@ -127,11 +279,121 @@ func main() {
}
if !waitUntilFinished {
return
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")
}
}
}

View File

@ -1,25 +0,0 @@
package main
import (
"os"
"github.com/chzyer/readline"
)
type filteredStdOut struct{}
func (s *filteredStdOut) Write(b []byte) (int, error) {
if len(b) == 1 && b[0] == 7 {
// ignore terminal bell from readline
return 0, nil
}
return os.Stdout.Write(b)
}
func (s *filteredStdOut) Close() error {
return os.Stdout.Close()
}
func init() {
readline.Stdout = &filteredStdOut{}
}

6
go.mod
View File

@ -3,6 +3,8 @@ module git.sr.ht/~tslocum/gmenu
go 1.12
require (
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/tslocum/promptui v0.3.4-0.20190628082230-cf53eafff9c5
github.com/jroimartin/gocui v0.4.0
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
)

43
go.sum
View File

@ -1,33 +1,10 @@
github.com/alecthomas/gometalinter v2.0.11+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk=
github.com/alecthomas/gometalinter v3.0.0+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/manifoldco/promptui v0.3.2 h1:rir7oByTERac6jhpHUPErHuopoRDvO3jxS+FdadEns8=
github.com/manifoldco/promptui v0.3.2/go.mod h1:8JU+igZ+eeiiRku4T5BjtKh2ms8sziGpSYl1gN8Bazw=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk=
github.com/tslocum/promptui v0.3.4-0.20190628082230-cf53eafff9c5 h1:ngqI2eodloV7WADE+TRxuXo9lrPLXnsn+7nQvwG8kBY=
github.com/tslocum/promptui v0.3.4-0.20190628082230-cf53eafff9c5/go.mod h1:ib2idig9p59EUDQJwRIN3/JHD8ThqsUu9fnPuA4zspA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/tools v0.0.0-20181122213734-04b5d21e00f1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
github.com/jroimartin/gocui v0.4.0 h1:52jnalstgmc25FmtGcWqa0tcbMEWS6RpFLsOIO+I+E8=
github.com/jroimartin/gocui v0.4.0/go.mod h1:7i7bbj99OgFHzo7kB2zPb8pXLqMBSQegY7azfqXMkyY=
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=
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=

View File

@ -10,8 +10,6 @@ import (
"strings"
)
// TODO: Support Type=URL
var (
scannedEntries []*DesktopEntry
scannedBytes []byte
@ -90,7 +88,6 @@ func UnquoteExec(ex string) string {
return ex
}
// TODO
func ExpandFieldCodes(ex string) string {
ex = strings.ReplaceAll(ex, "%F", "")
ex = strings.ReplaceAll(ex, "%f", "")