Use gocui for UI
parent
cc4942dcf7
commit
0dbc0fb4d3
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
6
go.mod
|
@ -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
43
go.sum
|
@ -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=
|
||||
|
|
|
@ -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", "")
|
||||
|
|
Loading…
Reference in New Issue