Add GTK interface
parent
7190fbb773
commit
5fdce165ba
62
README.md
62
README.md
|
@ -5,25 +5,61 @@
|
|||
|
||||
Desktop application launcher
|
||||
|
||||
## Warning: Experimental
|
||||
|
||||
Only a terminal interface has been implemented. A GTK interface is planned.
|
||||
|
||||
## Installation
|
||||
|
||||
```go get git.sr.ht/~tslocum/gmenu/cmd/gmenu```
|
||||
|
||||
## Usage
|
||||
|
||||
### GUI
|
||||
|
||||
*Coming soon*
|
||||
|
||||
### Console
|
||||
|
||||
sway/i3 + alacritty:
|
||||
```
|
||||
go get git.sr.ht/~tslocum/gmenu/cmd/gmenu
|
||||
```
|
||||
|
||||
### GUI
|
||||
|
||||
```
|
||||
go get git.sr.ht/~tslocum/gmenu/cmd/gtkmenu
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Console
|
||||
|
||||
```
|
||||
Usage of ./gmenu:
|
||||
-data-dirs string
|
||||
application data directories (default: $XDG_DATA_DIRS)
|
||||
-mouse
|
||||
enable mouse support
|
||||
-no-details
|
||||
hide application details
|
||||
-no-generic
|
||||
hide generic names
|
||||
```
|
||||
|
||||
### GUI
|
||||
|
||||
```
|
||||
Usage of ./gtkmenu:
|
||||
-data-dirs string
|
||||
application data directories (default: $XDG_DATA_DIRS)
|
||||
-height int
|
||||
window height (default 200)
|
||||
-no-generic
|
||||
hide generic names
|
||||
-width int
|
||||
window width (default 800)
|
||||
```
|
||||
|
||||
## Usage example - [sway](https://swaywm.org)/[i3](https://i3wm.org) + [alacritty](https://github.com/jwilm/alacritty)
|
||||
|
||||
### Console
|
||||
|
||||
```
|
||||
bindsym $mod+d exec --no-startup-id alacritty --class gmenu --title gmenu --working-directory ~ -e gmenu
|
||||
for_window [app_id="gmenu"] floating enable; resize set 745 105
|
||||
```
|
||||
|
||||
### GUI
|
||||
|
||||
```
|
||||
bindsym $mod+d exec --no-startup-id gtkmenu
|
||||
```
|
||||
|
|
|
@ -4,34 +4,35 @@ import (
|
|||
"github.com/jroimartin/gocui"
|
||||
)
|
||||
|
||||
func initGUI() (*gocui.Gui, error) {
|
||||
g, err := gocui.NewGui(gocui.OutputNormal)
|
||||
func initGUI() error {
|
||||
var err error
|
||||
gui, err = gocui.NewGui(gocui.OutputNormal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
g.InputEsc = true
|
||||
g.Cursor = true
|
||||
g.Mouse = !disableMouseSupport
|
||||
gui.InputEsc = true
|
||||
gui.Cursor = true
|
||||
gui.Mouse = config.EnableMouse
|
||||
|
||||
g.SetManagerFunc(layout)
|
||||
gui.SetManagerFunc(layout)
|
||||
|
||||
if err := keybindings(g); err != nil {
|
||||
return nil, err
|
||||
if err := keybindings(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go updateEntryInfo()
|
||||
|
||||
return g, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func layout(g *gocui.Gui) error {
|
||||
maxX, maxY := g.Size()
|
||||
func layout(_ *gocui.Gui) error {
|
||||
maxX, maxY := gui.Size()
|
||||
listWidth := maxX
|
||||
if !hideAppDetails {
|
||||
if !config.HideAppDetails {
|
||||
listWidth = maxX / 2
|
||||
|
||||
if v, err := g.SetView("ex", maxX/2, -1, maxX, 1); err != nil {
|
||||
if v, err := gui.SetView("ex", maxX/2, -1, maxX, 1); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
|
@ -41,7 +42,7 @@ func layout(g *gocui.Gui) error {
|
|||
v.Frame = false
|
||||
v.Wrap = false
|
||||
}
|
||||
if v, err := g.SetView("comment", maxX/2, 1, maxX, maxY); err != nil {
|
||||
if v, err := gui.SetView("comment", maxX/2, 1, maxX, maxY); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
|
@ -52,7 +53,7 @@ func layout(g *gocui.Gui) error {
|
|||
v.Wrap = true
|
||||
}
|
||||
}
|
||||
if v, err := g.SetView("list", -1, 0, listWidth, maxY); err != nil {
|
||||
if v, err := gui.SetView("list", -1, 0, listWidth, maxY); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
|
@ -64,10 +65,8 @@ func layout(g *gocui.Gui) error {
|
|||
v.Highlight = true
|
||||
v.SelBgColor = gocui.ColorGreen
|
||||
v.SelFgColor = gocui.ColorBlack
|
||||
|
||||
updateEntries("")
|
||||
}
|
||||
if v, err := g.SetView("main", -1, -1, listWidth, 1); err != nil {
|
||||
if v, err := gui.SetView("main", -1, -1, listWidth, 1); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
|
@ -80,7 +79,7 @@ func layout(g *gocui.Gui) error {
|
|||
v.Wrap = true
|
||||
v.Editor = gocui.EditorFunc(searchEditor)
|
||||
|
||||
if _, err := g.SetCurrentView("main"); err != nil {
|
||||
if _, err := gui.SetCurrentView("main"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -97,12 +96,12 @@ func closeGUI() {
|
|||
|
||||
gui.Close()
|
||||
|
||||
gui.Update(func(g *gocui.Gui) error {
|
||||
gui.Update(func(_ *gocui.Gui) error {
|
||||
return gocui.ErrQuit
|
||||
})
|
||||
}
|
||||
|
||||
func quit(g *gocui.Gui, v *gocui.View) error {
|
||||
func quit(_ *gocui.Gui, _ *gocui.View) error {
|
||||
closeGUI()
|
||||
return gocui.ErrQuit
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ package main
|
|||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~tslocum/gmenu/pkg/gmenu"
|
||||
"github.com/jroimartin/gocui"
|
||||
)
|
||||
|
||||
|
@ -31,30 +33,61 @@ func searchEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
|
|||
v.MoveCursor(1, 0, false)
|
||||
}
|
||||
|
||||
updateEntries(v.Buffer())
|
||||
gmenu.SetInput(strings.TrimSpace(v.Buffer()))
|
||||
}
|
||||
|
||||
func listPrev(g *gocui.Gui, v *gocui.View) error {
|
||||
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 listNext(g *gocui.Gui, v *gocui.View) error {
|
||||
func listNextEntry(_ *gocui.Gui, _ *gocui.View) error {
|
||||
list.MoveCursor(0, 1, false)
|
||||
updateEntryInfo()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listClickFromMouse(g *gocui.Gui, v *gocui.View) error {
|
||||
func listClickFromMouse(_ *gocui.Gui, _ *gocui.View) error {
|
||||
clickedList = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listSelectFromMouse(g *gocui.Gui, v *gocui.View) error {
|
||||
func listSelectFromMouse(_ *gocui.Gui, _ *gocui.View) error {
|
||||
if !clickedList {
|
||||
return nil
|
||||
}
|
||||
|
@ -62,46 +95,54 @@ func listSelectFromMouse(g *gocui.Gui, v *gocui.View) error {
|
|||
return listSelect()
|
||||
}
|
||||
|
||||
func listDeselectFromMouse(g *gocui.Gui, v *gocui.View) error {
|
||||
func listDeselectFromMouse(_ *gocui.Gui, _ *gocui.View) error {
|
||||
clickedList = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listSelectFromKey(g *gocui.Gui, v *gocui.View) error {
|
||||
func listSelectFromKey(_ *gocui.Gui, _ *gocui.View) error {
|
||||
return listSelect()
|
||||
}
|
||||
|
||||
func keybindings(g *gocui.Gui) error {
|
||||
if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, listPrev); err != nil {
|
||||
func keybindings() error {
|
||||
if err := gui.SetKeybinding("", gocui.KeyPgup, gocui.ModNone, listPrevPage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, listNext); err != nil {
|
||||
if err := gui.SetKeybinding("", gocui.KeyPgdn, gocui.ModNone, listNextPage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("", gocui.KeyEnter, gocui.ModNone, listSelectFromKey); err != nil {
|
||||
if err := gui.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, listPrevEntry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("list", gocui.MouseLeft, gocui.ModNone, listClickFromMouse); err != nil {
|
||||
if err := gui.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, listNextEntry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("list", gocui.MouseRelease, gocui.ModNone, listSelectFromMouse); err != nil {
|
||||
if err := gui.SetKeybinding("", gocui.KeyEnter, gocui.ModNone, listSelectFromKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("", gocui.MouseRelease, gocui.ModNone, listDeselectFromMouse); err != nil {
|
||||
if err := gui.SetKeybinding("list", gocui.MouseLeft, gocui.ModNone, listClickFromMouse); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
|
||||
if err := gui.SetKeybinding("list", gocui.MouseRelease, gocui.ModNone, listSelectFromMouse); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("", gocui.KeyEsc, gocui.ModNone, quit); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -3,95 +3,40 @@ package main
|
|||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~tslocum/desktop"
|
||||
"git.sr.ht/~tslocum/gmenu/pkg/config"
|
||||
"git.sr.ht/~tslocum/gmenu/pkg/gmenu"
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type listEntry struct {
|
||||
*desktop.Entry
|
||||
func updateEntries(input string) {
|
||||
gui.Update(func(_ *gocui.Gui) error {
|
||||
gmenu.FilterEntries()
|
||||
|
||||
originalIndex int
|
||||
label string
|
||||
}
|
||||
list.Clear()
|
||||
list.SetOrigin(0, 0)
|
||||
list.SetCursor(0, 0)
|
||||
defer updateEntryInfo()
|
||||
|
||||
func updateEntries(buf string) {
|
||||
buf = strings.TrimSpace(buf)
|
||||
emptyBuf := buf == ""
|
||||
|
||||
filteredEntries = nil
|
||||
if emptyBuf {
|
||||
for i, l := range desktopNames {
|
||||
filteredEntries = append(filteredEntries, &listEntry{originalIndex: i, label: l, Entry: desktopEntries[i]})
|
||||
}
|
||||
|
||||
sort.Slice(filteredEntries, func(i, j int) bool {
|
||||
ilower := strings.ToLower(filteredEntries[i].label)
|
||||
jlower := strings.ToLower(filteredEntries[j].label)
|
||||
|
||||
if ilower != jlower {
|
||||
return ilower < jlower
|
||||
} else {
|
||||
return i < j
|
||||
var printedEntry bool
|
||||
for _, entry := range gmenu.FilteredEntries {
|
||||
if printedEntry {
|
||||
fmt.Fprint(list, "\n")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
b := strings.ToLower(buf)
|
||||
|
||||
matches := fuzzy.RankFindFold(b, desktopNames)
|
||||
sort.Sort(matches)
|
||||
|
||||
for i, match := range matches {
|
||||
filteredEntries = append(filteredEntries, &listEntry{originalIndex: i, label: desktopNames[match.OriginalIndex], Entry: desktopEntries[match.OriginalIndex]})
|
||||
fmt.Fprint(list, entry.Label)
|
||||
printedEntry = true
|
||||
}
|
||||
|
||||
sort.Slice(filteredEntries, func(i, j int) bool {
|
||||
ilower := strings.ToLower(filteredEntries[i].label)
|
||||
jlower := strings.ToLower(filteredEntries[j].label)
|
||||
|
||||
ipre := strings.HasPrefix(ilower, b)
|
||||
jpre := strings.HasPrefix(jlower, b)
|
||||
|
||||
icon := strings.Contains(ilower, b)
|
||||
jcon := strings.Contains(jlower, b)
|
||||
|
||||
if ipre != jpre {
|
||||
return ipre && !jpre
|
||||
} else if icon != jcon {
|
||||
return icon && !jcon
|
||||
} else if ilower != jlower {
|
||||
return ilower < jlower
|
||||
} else {
|
||||
return i < j
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
list.Clear()
|
||||
list.SetOrigin(0, 0)
|
||||
list.SetCursor(0, 0)
|
||||
defer updateEntryInfo()
|
||||
|
||||
var printedEntry bool
|
||||
for _, entry := range filteredEntries {
|
||||
if printedEntry {
|
||||
fmt.Fprint(list, "\n")
|
||||
if printedEntry && input != "" {
|
||||
fmt.Fprint(list, "\n"+input)
|
||||
}
|
||||
|
||||
fmt.Fprint(list, entry.label)
|
||||
printedEntry = true
|
||||
}
|
||||
|
||||
if printedEntry && !emptyBuf {
|
||||
fmt.Fprint(list, "\n"+buf)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func selectedIndex() int {
|
||||
|
@ -111,15 +56,15 @@ func selectedEntry() *desktop.Entry {
|
|||
}
|
||||
|
||||
i := selectedIndex()
|
||||
if len(filteredEntries) == 0 || i < 0 || i > len(filteredEntries)-1 {
|
||||
if len(gmenu.FilteredEntries) == 0 || i < 0 || i > len(gmenu.FilteredEntries)-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return filteredEntries[i].Entry
|
||||
return gmenu.FilteredEntries[i].Entry
|
||||
}
|
||||
|
||||
func updateEntryInfo() {
|
||||
if hideAppDetails {
|
||||
if config.HideAppDetails {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -137,7 +82,7 @@ func updateEntryInfo() {
|
|||
if entry.Type == desktop.Application {
|
||||
exLine = entry.Exec
|
||||
} else { // Type == desktop.Link
|
||||
exLine = config.BrowserCommand + " " + entry.URL
|
||||
exLine = config.BrowserCommand() + " " + entry.URL
|
||||
}
|
||||
|
||||
comLine = entry.Comment
|
||||
|
@ -172,23 +117,20 @@ func listSelect() error {
|
|||
runInTerminal = entry.Terminal
|
||||
execute = entry.ExpandExec(shellquote.Join(flag.Args()...))
|
||||
} else { // Type == desktop.Link
|
||||
execute = shellquote.Join(config.BrowserCommand, entry.URL)
|
||||
execute = shellquote.Join(config.BrowserCommand(), entry.URL)
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
path := ""
|
||||
if entry != nil {
|
||||
path = entry.Path
|
||||
}
|
||||
|
||||
return run(runScript, path, waitUntilFinished, runInTerminal)
|
||||
err := gmenu.Run(&config.Config, execute, path, runInTerminal, waitUntilFinished)
|
||||
if err == nil {
|
||||
err = gocui.ErrQuit
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,43 +1,36 @@
|
|||
// gmenu - Desktop application launcher
|
||||
// https://git.sr.ht/~tslocum/gmenu
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~tslocum/desktop"
|
||||
"git.sr.ht/~tslocum/gmenu/pkg/gmenu"
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
var (
|
||||
dataDirs string
|
||||
disableMouseSupport bool
|
||||
hideAppDetails bool
|
||||
hideGenericNames bool
|
||||
type Config struct {
|
||||
gmenu.Config
|
||||
|
||||
desktopEntries []*desktop.Entry
|
||||
desktopNames []string
|
||||
filteredEntries []*listEntry
|
||||
EnableMouse bool
|
||||
}
|
||||
|
||||
var (
|
||||
config = &Config{}
|
||||
|
||||
gui *gocui.Gui
|
||||
input, comment, ex, list *gocui.View
|
||||
clickedList bool
|
||||
closedGUI bool
|
||||
done = make(chan bool)
|
||||
|
||||
closedGUI bool
|
||||
done = make(chan bool)
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetFlags(0)
|
||||
gmenu.SharedInit(&config.Config)
|
||||
|
||||
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")
|
||||
flag.BoolVar(&hideGenericNames, "no-generic", false, "hide generic names")
|
||||
flag.BoolVar(&config.EnableMouse, "mouse", false, "enable mouse support")
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
@ -48,48 +41,15 @@ func main() {
|
|||
log.Fatal("failed to start gmenu: non-interactive terminals are not supported")
|
||||
}
|
||||
|
||||
var dirs []string
|
||||
if dataDirs != "" {
|
||||
dirs = strings.Split(dataDirs, ":")
|
||||
} else {
|
||||
dirs = desktop.DataDirs()
|
||||
}
|
||||
gmenu.LoadEntries(&config.Config)
|
||||
|
||||
allEntries, err := desktop.Scan(dirs)
|
||||
err := initGUI()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
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 !hideGenericNames && entry.GenericName != "" {
|
||||
desktopEntries = append(desktopEntries, entry)
|
||||
desktopNames = append(desktopNames, entry.GenericName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gui, err = initGUI()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
go gmenu.HandleInput(updateEntries)
|
||||
gmenu.SetInput("")
|
||||
|
||||
go func() {
|
||||
if err := gui.MainLoop(); err != nil && err != gocui.ErrQuit {
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
// +build linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"git.sr.ht/~tslocum/gmenu/pkg/config"
|
||||
"github.com/jroimartin/gocui"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func run(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 gocui.ErrQuit
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
_, isExitErr := err.(*exec.ExitError)
|
||||
if err != nil && !isExitErr {
|
||||
return errors.Wrap(err, "failed to execute command")
|
||||
}
|
||||
|
||||
return gocui.ErrQuit
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
func run(runScript string, waitUntilFinished, runInTerminal bool) error {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,235 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~tslocum/desktop"
|
||||
"git.sr.ht/~tslocum/gmenu/pkg/gmenu"
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/tslocum/gotk3/gdk"
|
||||
"github.com/tslocum/gotk3/gtk"
|
||||
)
|
||||
|
||||
var execLabel *gtk.Label
|
||||
|
||||
func initList(container *gtk.Box) {
|
||||
inputView = newTextView()
|
||||
inputView.SetHExpand(false)
|
||||
inputView.SetVExpand(false)
|
||||
inputView.SetProperty("accepts-tab", false)
|
||||
inputView.SetProperty("wrap-mode", gtk.WRAP_CHAR)
|
||||
inputView.SetProperty("cursor-visible", false)
|
||||
inputView.SetProperty("left-margin", 2)
|
||||
inputView.SetProperty("right-margin", 2)
|
||||
|
||||
buffer, err := inputView.GetBuffer()
|
||||
if err != nil {
|
||||
log.Fatal("failed to create ListBox:", err)
|
||||
}
|
||||
|
||||
container.Add(inputView)
|
||||
|
||||
listScroll, err := gtk.ScrolledWindowNew(nil, nil)
|
||||
if err != nil {
|
||||
log.Fatal("failed to create ListBox:", err)
|
||||
}
|
||||
listScroll.SetHExpand(true)
|
||||
listScroll.SetVExpand(false)
|
||||
|
||||
listBox, err = gtk.ListBoxNew()
|
||||
if err != nil {
|
||||
log.Fatal("failed to create ListBox:", err)
|
||||
}
|
||||
listBox.SetSelectionMode(gtk.SELECTION_BROWSE)
|
||||
listBox.SetHExpand(false)
|
||||
|
||||
_, err = listBox.Connect("button-press-event", func(listBox *gtk.ListBox, ev *gdk.Event) {
|
||||
mouseEvent := &gdk.EventButton{ev}
|
||||
if mouseEvent.Type() == gdk.EVENT_2BUTTON_PRESS {
|
||||
err := listSelect(inputView)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal("failed to create ListBox:", err)
|
||||
}
|
||||
|
||||
listScroll.Add(listBox)
|
||||
container.PackEnd(listScroll, true, true, 0)
|
||||
|
||||
lastEntry := len(gmenu.FilteredEntries) - 1
|
||||
|
||||
for i, entry := range gmenu.FilteredEntries {
|
||||
row, err := gtk.ListBoxRowNew()
|
||||
if err != nil {
|
||||
log.Fatal("failed to create ListBoxRow:", err)
|
||||
}
|
||||
|
||||
l, err := gtk.LabelNew(entry.Label)
|
||||
if err != nil {
|
||||
log.Fatal("failed to create Label:", err)
|
||||
}
|
||||
|
||||
l.SetHAlign(gtk.ALIGN_START)
|
||||
|
||||
row.SetName("#" + strconv.Itoa(i))
|
||||
|
||||
if i == lastEntry {
|
||||
execLabel = l
|
||||
}
|
||||
|
||||
row.Add(l)
|
||||
listBox.Add(row)
|
||||
}
|
||||
|
||||
listBox.SetSortFunc(listSort, 0)
|
||||
|
||||
_, err = buffer.Connect("changed", func(tb *gtk.TextBuffer) bool {
|
||||
gmenu.SetInput(strings.TrimSpace(textViewText(inputView)))
|
||||
|
||||
return false
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal("failed to create ListBox:", err)
|
||||
}
|
||||
|
||||
gmenu.SetInput("")
|
||||
}
|
||||
|
||||
func updateList(input string) {
|
||||
listBox.InvalidateSort()
|
||||
|
||||
execLabel.SetText(input)
|
||||
|
||||
row := listBox.GetRowAtIndex(0)
|
||||
if row == nil {
|
||||
return
|
||||
}
|
||||
|
||||
listBox.SelectRow(row)
|
||||
|
||||
inputView.GrabFocus()
|
||||
}
|
||||
|
||||
func newBox(orient gtk.Orientation) *gtk.Box {
|
||||
box, err := gtk.BoxNew(orient, 0)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to create box:", err)
|
||||
}
|
||||
return box
|
||||
}
|
||||
|
||||
func newTextView() *gtk.TextView {
|
||||
tv, err := gtk.TextViewNew()
|
||||
if err != nil {
|
||||
log.Fatal("Unable to create TextView:", err)
|
||||
}
|
||||
return tv
|
||||
}
|
||||
|
||||
func textViewBuffer(tv *gtk.TextView) *gtk.TextBuffer {
|
||||
buffer, err := tv.GetBuffer()
|
||||
if err != nil {
|
||||
log.Fatal("Unable to get TextView buffer:", err)
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
func textViewText(tv *gtk.TextView) string {
|
||||
buffer := textViewBuffer(tv)
|
||||
start, end := buffer.GetBounds()
|
||||
|
||||
text, err := buffer.GetText(start, end, true)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to get TextView text:", err)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func selectedIndex() int {
|
||||
if listBox == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return rowID(listBox.GetSelectedRow())
|
||||
}
|
||||
|
||||
func selectedEntry() *desktop.Entry {
|
||||
i := selectedIndex()
|
||||
if len(gmenu.FilteredEntries) == 0 || i < 0 || i > len(gmenu.FilteredEntries)-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gmenu.FilteredEntries[i].Entry
|
||||
}
|
||||
|
||||
func listSelect(_ *gtk.TextView) error {
|
||||
gmenu.CloseInput()
|
||||
|
||||
var (
|
||||
execute string
|
||||
runInTerminal bool
|
||||
waitUntilFinished bool
|
||||
)
|
||||
entry := selectedEntry()
|
||||
if entry == nil {
|
||||
waitUntilFinished = true
|
||||
execute = textViewText(inputView)
|
||||
} else if entry.Type == desktop.Application {
|
||||
runInTerminal = entry.Terminal
|
||||
execute = entry.ExpandExec(shellquote.Join(flag.Args()...))
|
||||
} else { // Type == desktop.Link
|
||||
execute = shellquote.Join(config.BrowserCommand(), entry.URL)
|
||||
}
|
||||
|
||||
path := ""
|
||||
if entry != nil {
|
||||
path = entry.Path
|
||||
}
|
||||
|
||||
err := gmenu.Run(&config.Config, execute, path, runInTerminal, waitUntilFinished)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func listSort(row1 *gtk.ListBoxRow, row2 *gtk.ListBoxRow, userData uintptr) int {
|
||||
r1 := rowID(row1)
|
||||
r2 := rowID(row2)
|
||||
if r1 < 0 || r2 < 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
if gmenu.Sort(r1, r2) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func rowID(row *gtk.ListBoxRow) int {
|
||||
if row == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
name, err := row.GetName()
|
||||
if err != nil || len(name) < 2 || name[0] != '#' {
|
||||
return -1
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(name[1:])
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
|
@ -1,3 +1,138 @@
|
|||
package gtkmenu
|
||||
package main
|
||||
|
||||
func main() {}
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.sr.ht/~tslocum/gmenu/pkg/gmenu"
|
||||
"github.com/gotk3/gotk3/glib"
|
||||
"github.com/tslocum/gotk3/gdk"
|
||||
"github.com/tslocum/gotk3/gtk"
|
||||
)
|
||||
|
||||
const (
|
||||
appID = "space.rocketnine.gmenu"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
gmenu.Config
|
||||
|
||||
Width, Height int
|
||||
}
|
||||
|
||||
var (
|
||||
config = &Config{}
|
||||
listBox *gtk.ListBox
|
||||
inputView *gtk.TextView
|
||||
)
|
||||
|
||||
func init() {
|
||||
gmenu.SharedInit(&config.Config)
|
||||
|
||||
flag.IntVar(&config.Width, "width", 800, "window width")
|
||||
flag.IntVar(&config.Height, "height", 200, "window height")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
application, err := gtk.ApplicationNew(appID, glib.APPLICATION_FLAGS_NONE)
|
||||
if err != nil {
|
||||
log.Fatal("failed to create application:", err)
|
||||
}
|
||||
|
||||
_, err = application.Connect("activate", func() { onActivate(application) })
|
||||
if err != nil {
|
||||
log.Fatal("failed to connect while creating application:", err)
|
||||
}
|
||||
|
||||
os.Exit(application.Run(os.Args))
|
||||
}
|
||||
|
||||
func onActivate(application *gtk.Application) {
|
||||
appWindow, err := gtk.ApplicationWindowNew(application)
|
||||
if err != nil {
|
||||
log.Fatal("failed to create application window:", err)
|
||||
}
|
||||
|
||||
_, err = appWindow.Connect("destroy", func() {
|
||||
os.Exit(0)
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal("failed to create application window:", err)
|
||||
}
|
||||
|
||||
gmenu.LoadEntries(&config.Config)
|
||||
gmenu.FilterEntries()
|
||||
|
||||
go gmenu.HandleInput(func(input string) {
|
||||
_, err := glib.IdleAdd(updateList, input)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
gmenu.Entries = append(gmenu.Entries, nil)
|
||||
gmenu.Names = append(gmenu.Names, "")
|
||||
gmenu.FilteredEntries = append(gmenu.FilteredEntries, &gmenu.ListEntry{Label: "", Entry: nil})
|
||||
|
||||
container := newBox(gtk.ORIENTATION_VERTICAL)
|
||||
initList(container)
|
||||
|
||||
appWindow.Add(container)
|
||||
|
||||
appWindow.SetTitle("gmenu")
|
||||
|
||||
appWindow.SetResizable(false)
|
||||
appWindow.SetSizeRequest(config.Width, config.Height)
|
||||
appWindow.SetDefaultSize(config.Width, config.Height)
|
||||
appWindow.SetPosition(gtk.WIN_POS_CENTER)
|
||||
|
||||
appWindow.SetDecorated(false)
|
||||
appWindow.SetBorderWidth(0)
|
||||
appWindow.Stick()
|
||||
appWindow.SetKeepAbove(true)
|
||||
|
||||
appWindow.ShowAll()
|
||||
|
||||
_, err = appWindow.Connect("key-press-event", func(win *gtk.ApplicationWindow, ev *gdk.Event) bool {
|
||||
keyEvent := &gdk.EventKey{ev}
|
||||
switch keyEvent.KeyVal() {
|
||||
case gdk.KEY_Up, gdk.KEY_Down:
|
||||
offset := -1
|
||||
if keyEvent.KeyVal() == gdk.KEY_Down {
|
||||
offset = 1
|
||||
}
|
||||
|
||||
index := 0
|
||||
row := listBox.GetSelectedRow()
|
||||
if row != nil {
|
||||
index = row.GetIndex()
|
||||
}
|
||||
|
||||
row = listBox.GetRowAtIndex(index + offset)
|
||||
if row != nil {
|
||||
listBox.SelectRow(row)
|
||||
row.GrabFocus()
|
||||
inputView.GrabFocus()
|
||||
}
|
||||
|
||||
return true
|
||||
case gdk.KEY_Return:
|
||||
err = listSelect(inputView)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return true
|
||||
case gdk.KEY_Escape:
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal("failed to connect key-press-event:", err)
|
||||
}
|
||||
}
|
||||
|
|
2
go.mod
2
go.mod
|
@ -4,6 +4,7 @@ go 1.12
|
|||
|
||||
require (
|
||||
git.sr.ht/~tslocum/desktop v0.1.1
|
||||
github.com/gotk3/gotk3 v0.0.0-20190620081259-6dcdf9e5c51e
|
||||
github.com/jroimartin/gocui v0.4.0
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/lithammer/fuzzysearch v1.0.2
|
||||
|
@ -11,5 +12,6 @@ require (
|
|||
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
|
||||
github.com/tslocum/gotk3 v0.0.0-20190727120037-6cb68ea19890
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect
|
||||
)
|
||||
|
|
7
go.sum
7
go.sum
|
@ -1,7 +1,7 @@
|
|||
git.sr.ht/~tslocum/desktop v0.1.0 h1:cfOcbsBA/XOkemwRjqZ9N5oeYGnGlnC6Vq976aLrrS8=
|
||||
git.sr.ht/~tslocum/desktop v0.1.0/go.mod h1:cUn0Q8ALjkAq40qSei795yN3CfO5pkeYKo2gmzaZ2SI=
|
||||
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/gotk3/gotk3 v0.0.0-20190620081259-6dcdf9e5c51e h1:KFy3swDjmbaSAE6b1iExIgsYt0OkfoLP3HjLm4ifSR8=
|
||||
github.com/gotk3/gotk3 v0.0.0-20190620081259-6dcdf9e5c51e/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=
|
||||
|
@ -16,6 +16,9 @@ github.com/nsf/termbox-go v0.0.0-20190624072549-eeb6cd0a1762 h1:44Lv0bNi88GweB54
|
|||
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=
|
||||
github.com/tslocum/gotk3 v0.0.0-20190727120037-6cb68ea19890 h1:E4ShJr9mkfrOgv2gJfLhuc8s1uFfWsTG24h2JDIjrVs=
|
||||
github.com/tslocum/gotk3 v0.0.0-20190727120037-6cb68ea19890/go.mod h1:lKW1BMxjgJ9vFqOZ2pNvC6p6ln6en3KqvV4KRt5FLB4=
|
||||
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=
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI=
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
|
@ -4,29 +4,31 @@ before:
|
|||
hooks:
|
||||
- go mod download
|
||||
builds:
|
||||
- binary: gmenu
|
||||
-
|
||||
id: gmenu
|
||||
binary: gmenu
|
||||
main: ./cmd/gmenu
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
ldflags:
|
||||
- -s -w -X git.sr.ht/~tslocum/gmenu/pkg/config.Version={{.Version}}
|
||||
- -s -w -X git.sr.ht/~tslocum/gmenu/pkg/gmenu.Version={{.Version}}
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- binary: gtkmenu
|
||||
-
|
||||
id: gtkmenu
|
||||
binary: gtkmenu
|
||||
main: ./cmd/gtkmenu
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
ldflags:
|
||||
- -s -w -X git.sr.ht/~tslocum/gmenu/pkg/config.Version={{.Version}}
|
||||
- -s -w -X git.sr.ht/~tslocum/gmenu/pkg/gmenu.Version={{.Version}}
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
# - windows
|
||||
goarch:
|
||||
- 386
|
||||
# - 386
|
||||
- amd64
|
||||
archive:
|
||||
replacements:
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
package config
|
||||
|
||||
var (
|
||||
Version string
|
||||
TerminalCommand = "i3-sensible-terminal"
|
||||
BrowserCommand = "xdg-open"
|
||||
)
|
Loading…
Reference in New Issue