353 lines
7.3 KiB
Go
353 lines
7.3 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"html"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gotk3/gotk3/pango"
|
|
|
|
"git.sr.ht/~tslocum/desktop"
|
|
"git.sr.ht/~tslocum/gmenu/pkg/gmenu"
|
|
"github.com/gotk3/gotk3/gdk"
|
|
"github.com/kballard/go-shellquote"
|
|
"github.com/tslocum/gotk3/gtk"
|
|
gtkfork "github.com/tslocum/gotk3/gtk"
|
|
)
|
|
|
|
const (
|
|
iconSize = 48
|
|
iconMargin = 4
|
|
iconMarginStart = 2
|
|
labelMarginStart = 4
|
|
labelMarginTop = 8
|
|
labelMarginTopComment = 4
|
|
)
|
|
|
|
var execLabel *gtk.Label
|
|
|
|
func initList(container *gtk.Box) {
|
|
inputView = newTextView()
|
|
setNoExpand(&inputView.Widget)
|
|
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 *gtkfork.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)
|
|
}
|
|
|
|
row.SetName("#" + strconv.Itoa(i))
|
|
|
|
container := newBox(gtk.ORIENTATION_HORIZONTAL)
|
|
|
|
if !config.HideAppIcons {
|
|
s, _ := container.GetScreen()
|
|
theme, err := gtk.IconThemeGetForScreen(*s)
|
|
if err != nil {
|
|
log.Fatal("failed to get icon theme:", err)
|
|
}
|
|
|
|
var (
|
|
pbuf *gdk.Pixbuf
|
|
img *gtk.Image
|
|
)
|
|
|
|
if entry.Entry != nil && entry.Icon != "" {
|
|
if path.IsAbs(entry.Icon) {
|
|
pbuf, err = gdk.PixbufNewFromFileAtSize(entry.Icon, iconSize, iconSize)
|
|
} else {
|
|
pbuf, err = theme.LoadIcon(entry.Icon, iconSize, gtk.ICON_LOOKUP_USE_BUILTIN)
|
|
}
|
|
}
|
|
if pbuf == nil || err != nil {
|
|
var icon string
|
|
if entry.Entry == nil {
|
|
icon = "utilities-terminal"
|
|
} else if entry.Type == desktop.Application {
|
|
icon = "application-x-executable"
|
|
} else {
|
|
icon = "text-html"
|
|
}
|
|
pbuf, err = theme.LoadIcon(icon, iconSize, gtk.ICON_LOOKUP_USE_BUILTIN)
|
|
}
|
|
if err != nil {
|
|
// Failed to load icon
|
|
|
|
img, err = gtk.ImageNew()
|
|
if err == nil {
|
|
img.SetSizeRequest(iconSize, iconSize)
|
|
}
|
|
} else {
|
|
if pbuf.GetWidth() != iconSize && pbuf.GetHeight() != iconSize {
|
|
pbuf, _ = pbuf.ScaleSimple(iconSize, iconSize, gdk.INTERP_BILINEAR)
|
|
}
|
|
|
|
img, err = gtk.ImageNewFromPixbuf(pbuf)
|
|
}
|
|
if err != nil {
|
|
log.Fatal("failed to create Icon:", err)
|
|
}
|
|
|
|
img.SetMarginStart(iconMarginStart)
|
|
img.SetMarginTop(iconMargin)
|
|
img.SetMarginEnd(iconMargin)
|
|
img.SetMarginBottom(iconMargin)
|
|
container.PackStart(img, false, false, 0)
|
|
}
|
|
|
|
labelContainer := newBox(gtk.ORIENTATION_VERTICAL)
|
|
labelContainer.SetMarginStart(labelMarginStart)
|
|
|
|
l, err := gtk.LabelNew(fmt.Sprintf("<b>%s</b>", html.EscapeString(entry.Label)))
|
|
if err != nil {
|
|
log.Fatal("failed to create Label:", err)
|
|
}
|
|
l.SetUseMarkup(true)
|
|
|
|
l.SetHAlign(gtk.ALIGN_START)
|
|
setNoExpand(&l.Widget)
|
|
l.SetLineWrap(false)
|
|
l.SetSingleLineMode(true)
|
|
l.SetEllipsize(pango.ELLIPSIZE_END)
|
|
|
|
if !config.HideAppIcons {
|
|
l.SetMarginTop(labelMarginTop)
|
|
}
|
|
|
|
labelContainer.PackStart(l, false, false, 0)
|
|
|
|
if entry.Entry == nil || (entry.Entry != nil && entry.Comment != "") {
|
|
comment := ""
|
|
if entry.Entry != nil {
|
|
comment = entry.Comment
|
|
}
|
|
l, err := gtk.LabelNew(comment)
|
|
if err != nil {
|
|
log.Fatal("failed to create Label:", err)
|
|
}
|
|
|
|
l.SetHAlign(gtk.ALIGN_START)
|
|
setNoExpand(&l.Widget)
|
|
l.SetLineWrap(false)
|
|
l.SetSingleLineMode(true)
|
|
l.SetEllipsize(pango.ELLIPSIZE_END)
|
|
|
|
if !config.HideAppIcons {
|
|
l.SetMarginTop(labelMarginTopComment)
|
|
}
|
|
|
|
labelContainer.Add(l)
|
|
|
|
if i == lastEntry {
|
|
execLabel = l
|
|
}
|
|
}
|
|
|
|
setNoExpand(&labelContainer.Widget)
|
|
container.Add(labelContainer)
|
|
|
|
setNoExpand(&container.Widget)
|
|
row.Add(container)
|
|
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 setNoExpand(v *gtk.Widget) {
|
|
v.SetHExpand(false)
|
|
v.SetVExpand(false)
|
|
}
|
|
|
|
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
|
|
}
|