Use library tview for terminal interface

This commit is contained in:
Trevor Slocum 2019-09-30 17:26:44 -07:00
parent 258a8768c2
commit ca559957f1
7 changed files with 203 additions and 315 deletions

View File

@ -1,3 +1,8 @@
0.2.2:
- Resolve search not being case-insensitive
- gmenu: Use library tview for terminal interface
- gtkmenu: Run applications in terminal on CTL+Enter
0.2.1:
- Minor GTK interface updates

View File

@ -1,91 +1,177 @@
package main
import (
"github.com/jroimartin/gocui"
"strings"
"git.sr.ht/~tslocum/gmenu/pkg/gmenu"
"github.com/gdamore/tcell"
"github.com/rivo/tview"
)
func initGUI() error {
var err error
gui, err = gocui.NewGui(gocui.OutputNormal)
if err != nil {
return err
}
var (
app *tview.Application
inputView *tview.InputField
optionsList *OptionsList
appDetailsView *tview.TextView
gui.InputEsc = true
gui.Cursor = true
gui.Mouse = config.EnableMouse
closedGUI bool
)
gui.SetManagerFunc(layout)
if err := keybindings(); err != nil {
return err
}
go updateEntryInfo()
return nil
type OptionsList struct {
*tview.TextView
options []string
origin int
selected int
shown int
}
func layout(_ *gocui.Gui) error {
maxX, maxY := gui.Size()
listWidth := maxX
if !config.HideAppDetails {
listWidth = maxX / 2
func NewOptionsList(options []string) *OptionsList {
opts := OptionsList{
TextView: tview.NewTextView(),
options: options,
}
if v, err := gui.SetView("ex", maxX/2, -1, maxX, 1); err != nil {
if err != gocui.ErrUnknownView {
return err
opts.TextView = opts.SetDynamicColors(true).SetWordWrap(false)
return &opts
}
func (r *OptionsList) Draw(screen tcell.Screen) {
_, height := screen.Size()
var b strings.Builder
r.shown = 0
for i, option := range r.options {
if i < r.origin || i-r.origin >= height-1 {
continue
}
if i == r.selected {
b.WriteString(`[::r]`)
}
if i-r.origin < height-2 {
b.WriteString(option + "\n")
} else {
b.WriteString(option)
}
if i == r.selected {
b.WriteString(`[-:-:-]`)
}
r.shown++
}
r.TextView.SetText(b.String()).Highlight("gmenu").Draw(screen)
}
func initGUI() (*tview.Application, error) {
app = tview.NewApplication()
inputView = tview.NewInputField().
SetLabel("").
SetFieldWidth(0).
SetFieldBackgroundColor(tcell.ColorDefault).
SetFieldTextColor(tcell.ColorDefault).
SetChangedFunc(func(text string) {
gmenu.SetInput(text)
})
optionsList = NewOptionsList(nil)
grid := tview.NewGrid().
SetBorders(false).
SetRows(1, -1)
appDetailsView = tview.NewTextView().
SetTextAlign(tview.AlignLeft).
SetWrap(true).
SetWordWrap(true)
if config.HideAppDetails {
grid = grid.SetColumns(-1).
AddItem(inputView, 0, 0, 1, 1, 0, 0, true).
AddItem(optionsList, 1, 0, 1, 1, 0, 0, false)
} else {
grid = grid.SetColumns(-1, -1).
AddItem(inputView, 0, 0, 1, 2, 0, 0, true).
AddItem(optionsList, 1, 0, 1, 1, 0, 0, false).
AddItem(appDetailsView, 1, 1, 1, 1, 0, 0, false)
}
app = app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyUp {
if optionsList.origin > 0 && optionsList.selected == optionsList.origin {
optionsList.origin--
}
if optionsList.selected > 0 {
optionsList.selected--
}
event = nil
updateEntryInfo()
app.Draw()
} else if event.Key() == tcell.KeyDown {
if optionsList.selected < len(optionsList.options)-1 {
optionsList.selected++
if optionsList.selected > optionsList.origin+optionsList.shown-1 {
optionsList.origin++
}
}
event = nil
updateEntryInfo()
app.Draw()
} else if event.Key() == tcell.KeyPgUp {
if optionsList.origin == 0 {
optionsList.selected = 0
updateEntryInfo()
app.Draw()
return nil
}
ex = v
optionsList.origin -= optionsList.shown - 2
if optionsList.origin < 0 {
optionsList.origin = 0
}
optionsList.selected = optionsList.origin
v.Frame = false
v.Wrap = false
}
if v, err := gui.SetView("comment", maxX/2, 1, maxX, maxY); err != nil {
if err != gocui.ErrUnknownView {
return err
updateEntryInfo()
app.Draw()
return nil
} else if event.Key() == tcell.KeyPgDn {
numEntries := len(gmenu.FilteredEntries)
if optionsList.origin >= numEntries-optionsList.shown {
optionsList.selected = numEntries - 1
updateEntryInfo()
app.Draw()
return nil
}
comment = v
optionsList.origin += optionsList.shown - 2
if optionsList.origin > numEntries-optionsList.shown {
optionsList.origin = numEntries - optionsList.shown
}
optionsList.selected = optionsList.origin
v.Frame = false
v.Wrap = true
}
}
if v, err := gui.SetView("list", -1, 0, listWidth, maxY); err != nil {
if err != gocui.ErrUnknownView {
return err
updateEntryInfo()
app.Draw()
return nil
} else if event.Key() == tcell.KeyEnter {
err := listSelect()
if err != nil {
panic(err)
}
} else if event.Key() == tcell.KeyEscape {
done <- true
}
return event
})
list = v
app = app.SetRoot(grid, true)
v.Frame = false
v.Wrap = false
v.Highlight = true
v.SelBgColor = gocui.ColorGreen
v.SelFgColor = gocui.ColorBlack
}
if v, err := gui.SetView("main", -1, -1, listWidth, 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 := gui.SetCurrentView("main"); err != nil {
return err
}
}
_, _ = maxX, maxY
return nil
go gmenu.HandleInput(updateEntries)
gmenu.SetInput("")
return app, nil
}
func closeGUI() {
@ -94,14 +180,5 @@ func closeGUI() {
}
closedGUI = true
gui.Close()
gui.Update(func(_ *gocui.Gui) error {
return gocui.ErrQuit
})
}
func quit(_ *gocui.Gui, _ *gocui.View) error {
closeGUI()
return gocui.ErrQuit
app.Stop()
}

View File

@ -1,150 +0,0 @@
package main
import (
"log"
"strings"
"git.sr.ht/~tslocum/gmenu/pkg/gmenu"
"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)
}
gmenu.SetInput(strings.TrimSpace(v.Buffer()))
}
func listPrevPage(_ *gocui.Gui, _ *gocui.View) error {
_, size := list.Size()
_, o := list.Origin()
if o == 0 {
return list.SetCursor(0, 0)
}
o -= size - 2
if o < 0 {
o = 0
}
return list.SetOrigin(0, o)
}
func listNextPage(_ *gocui.Gui, _ *gocui.View) error {
numEntries := len(gmenu.FilteredEntries)
_, size := list.Size()
_, o := list.Origin()
if o >= numEntries-size {
return list.SetCursor(0, size-1)
}
o += size - 2
if o > numEntries-size {
o = numEntries - size
}
return list.SetOrigin(0, o)
}
func listPrevEntry(_ *gocui.Gui, _ *gocui.View) error {
list.MoveCursor(0, -1, false)
updateEntryInfo()
return nil
}
func listNextEntry(_ *gocui.Gui, _ *gocui.View) error {
list.MoveCursor(0, 1, false)
updateEntryInfo()
return nil
}
func listClickFromMouse(_ *gocui.Gui, _ *gocui.View) error {
clickedList = true
return nil
}
func listSelectFromMouse(_ *gocui.Gui, _ *gocui.View) error {
if !clickedList {
return nil
}
return listSelect()
}
func listDeselectFromMouse(_ *gocui.Gui, _ *gocui.View) error {
clickedList = false
return nil
}
func listSelectFromKey(_ *gocui.Gui, _ *gocui.View) error {
return listSelect()
}
func keybindings() error {
if err := gui.SetKeybinding("", gocui.KeyPgup, gocui.ModNone, listPrevPage); err != nil {
return err
}
if err := gui.SetKeybinding("", gocui.KeyPgdn, gocui.ModNone, listNextPage); err != nil {
return err
}
if err := gui.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, listPrevEntry); err != nil {
return err
}
if err := gui.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, listNextEntry); 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
}

View File

@ -4,57 +4,37 @@ import (
"flag"
"fmt"
"strings"
"time"
"git.sr.ht/~tslocum/desktop"
"git.sr.ht/~tslocum/gmenu/pkg/gmenu"
"github.com/jroimartin/gocui"
"github.com/kballard/go-shellquote"
)
func updateEntries(input string) {
gui.Update(func(_ *gocui.Gui) error {
gmenu.FilterEntries()
gmenu.FilterEntries()
list.Clear()
list.SetOrigin(0, 0)
list.SetCursor(0, 0)
defer updateEntryInfo()
optionsList.selected = 0
optionsList.origin = 0
defer updateEntryInfo()
var printedEntry bool
for _, entry := range gmenu.FilteredEntries {
if printedEntry {
fmt.Fprint(list, "\n")
}
fmt.Fprint(list, entry.Label)
printedEntry = true
}
if printedEntry && input != "" {
fmt.Fprint(list, "\n"+input)
}
return nil
})
optionsList.options = nil
for _, entry := range gmenu.FilteredEntries {
optionsList.options = append(optionsList.options, entry.Label)
}
if input != "" {
optionsList.options = append(optionsList.options, input)
}
}
func selectedIndex() int {
if list == nil {
if optionsList == nil {
return -1
}
_, selectedOrigin := list.Origin()
_, selectedCursor := list.Cursor()
return selectedOrigin + selectedCursor
return optionsList.selected
}
func selectedEntry() *desktop.Entry {
if list == nil {
return nil
}
i := selectedIndex()
if len(gmenu.FilteredEntries) == 0 || i < 0 || i > len(gmenu.FilteredEntries)-1 {
return nil
@ -68,14 +48,6 @@ func updateEntryInfo() {
return
}
for {
if list != nil && comment != nil && ex != nil {
break
}
time.Sleep(time.Millisecond)
}
var exLine, comLine string
entry := selectedEntry()
if entry != nil {
@ -87,18 +59,12 @@ func updateEntryInfo() {
comLine = entry.Comment
} else {
exLine = "bash -c "
if input != nil {
exLine += strings.TrimSpace(input.Buffer())
}
exLine = fmt.Sprintf("bash -c %s", strings.TrimSpace(inputView.GetText()))
comLine = "Shell command"
}
ex.Clear()
fmt.Fprint(ex, exLine)
comment.Clear()
fmt.Fprint(comment, comLine)
appDetailsView = appDetailsView.SetText(exLine + "\n\n" + comLine)
}
func listSelect() error {
@ -112,7 +78,7 @@ func listSelect() error {
entry := selectedEntry()
if entry == nil {
waitUntilFinished = true
execute = input.Buffer()
execute = inputView.GetText()
} else if entry.Type == desktop.Application {
if entry.Terminal {
runInTerminal = true
@ -130,10 +96,5 @@ func listSelect() error {
path = entry.Path
}
err := gmenu.Run(&config.Config, execute, path, runInTerminal, waitUntilFinished)
if err == nil {
err = gocui.ErrQuit
}
return err
return gmenu.Run(&config.Config, execute, path, runInTerminal, waitUntilFinished)
}

View File

@ -6,7 +6,6 @@ import (
"os"
"git.sr.ht/~tslocum/gmenu/pkg/gmenu"
"github.com/jroimartin/gocui"
"github.com/mattn/go-isatty"
)
@ -19,12 +18,7 @@ type Config struct {
var (
config = &Config{}
gui *gocui.Gui
input, comment, ex, list *gocui.View
clickedList bool
closedGUI bool
done = make(chan bool)
done = make(chan bool)
)
func init() {
@ -43,17 +37,14 @@ func main() {
gmenu.LoadEntries(&config.Config)
err := initGUI()
app, err := initGUI()
if err != nil {
log.Fatal(err)
panic(err)
}
go gmenu.HandleInput(updateEntries)
gmenu.SetInput("")
go func() {
if err := gui.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Fatal(err)
if err := app.Run(); err != nil {
panic(err)
}
done <- true
@ -61,9 +52,5 @@ func main() {
<-done
if !closedGUI {
closedGUI = true
gui.Close()
}
closeGUI()
}

7
go.mod
View File

@ -4,13 +4,12 @@ go 1.12
require (
git.sr.ht/~tslocum/desktop v0.1.1
github.com/gdamore/tcell v1.1.2
github.com/gotk3/gotk3 v0.0.0-20190827191254-95d4bac6fe1b
github.com/jroimartin/gocui v0.4.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/lithammer/fuzzysearch v1.0.2
github.com/mattn/go-isatty v0.0.9
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317 // indirect
github.com/pkg/errors v0.8.1
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 // indirect
github.com/rivo/tview v0.0.0-20190829161255-f8bc69b90341
golang.org/x/sys v0.0.0-20190927073244-c990c680b611 // indirect
)

23
go.sum
View File

@ -1,22 +1,31 @@
git.sr.ht/~tslocum/desktop v0.1.1 h1:hS1DgT1Ur0DR42Z4vr+Zsasjjd8M9PVwIEmeAd1xLS4=
git.sr.ht/~tslocum/desktop v0.1.1/go.mod h1:cUn0Q8ALjkAq40qSei795yN3CfO5pkeYKo2gmzaZ2SI=
github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.1.2 h1:Afe8cU6SECC06UmvaJ55Jr3Eh0tz/ywLjqWYqjGZp3s=
github.com/gdamore/tcell v1.1.2/go.mod h1:h3kq4HO9l2On+V9ed8w8ewqQEmGCSSHOgQ+2h8uzurE=
github.com/gotk3/gotk3 v0.0.0-20190827191254-95d4bac6fe1b h1:/ExhbPkho7qhFp96P5JZq5GB2x3ApecVIy0pqPoF0DQ=
github.com/gotk3/gotk3 v0.0.0-20190827191254-95d4bac6fe1b/go.mod h1:Eew3QBwAOBTrfFFDmsDE5wZWbcagBL1NUslj1GhRveo=
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/lithammer/fuzzysearch v1.0.2 h1:AjCE2iwc5y+8K+h2nXVc0Pmrpjvu+JVqMgiZ0oakXDM=
github.com/lithammer/fuzzysearch v1.0.2/go.mod h1:bvAJyokfCQ7Vknrd4Kgc+izmMrPj5CiBAu2t6rK1Kak=
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
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-20190817171036-93860e161317 h1:hhGN4SFXgXo61Q4Sjj/X9sBjyeSa2kdpaOzCO+8EVQw=
github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317/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-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
github.com/rivo/tview v0.0.0-20190829161255-f8bc69b90341 h1:d2Z5U4d3fenPRFFweaMCogbXiRywM5kgYtu20/hol3M=
github.com/rivo/tview v0.0.0-20190829161255-f8bc69b90341/go.mod h1:+rKjP5+h9HMwWRpAfhIkkQ9KE3m3Nz5rwn7YtUpwgqk=
github.com/rivo/uniseg v0.0.0-20190513083848-b9f5b9457d44 h1:XKCbzPvK4/BbMXoMJOkYP2ANxiAEO0HM1xn6psSbXxY=
github.com/rivo/uniseg v0.0.0-20190513083848-b9f5b9457d44/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=