initial commit
This commit is contained in:
22
Makefile
Normal file
22
Makefile
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
get:
|
||||||
|
go get github.com/gotk3/gotk3
|
||||||
|
go get github.com/gotk3/gotk3/gdk
|
||||||
|
go get github.com/gotk3/gotk3/glib
|
||||||
|
go get github.com/dlasky/gotk3-layershell/layershell
|
||||||
|
go get github.com/joshuarubin/go-sway
|
||||||
|
go get github.com/allan-simon/go-singleinstance
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build -o bin/nwg-menu *.go
|
||||||
|
|
||||||
|
install:
|
||||||
|
mkdir -p /usr/share/nwg-menu
|
||||||
|
cp -r desktop-directories /usr/share/nwg-menu
|
||||||
|
cp menu-start.css /usr/share/nwg-menu
|
||||||
|
cp bin/nwg-menu /usr/bin
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm /usr/bin/nwg-menu
|
||||||
|
|
||||||
|
run:
|
||||||
|
go run *.go
|
||||||
10
go.mod
Normal file
10
go.mod
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module github.com/nwg-piotr/nwg-menu
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
||||||
|
github.com/dlasky/gotk3-layershell v0.0.0-20210331230524-5cca0b819261
|
||||||
|
github.com/gotk3/gotk3 v0.5.3-0.20210223154815-289cfb6dbf32
|
||||||
|
github.com/joshuarubin/go-sway v0.0.4
|
||||||
|
)
|
||||||
25
go.sum
Normal file
25
go.sum
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
|
||||||
|
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.mod h1:6AXRstqK+32jeFmw89QGL2748+dj34Av4xc/I9oo9BY=
|
||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dlasky/gotk3-layershell v0.0.0-20210331230524-5cca0b819261 h1:eoXn91ckLWKMXmQKX34UHEF2XMyQpRnnP80fDiu+kys=
|
||||||
|
github.com/dlasky/gotk3-layershell v0.0.0-20210331230524-5cca0b819261/go.mod h1:d56Gslp3IaiT8lqxD/lO1Msz1wYgD8D/HTKHgSdg9tU=
|
||||||
|
github.com/gotk3/gotk3 v0.5.3-0.20210223154815-289cfb6dbf32 h1:wE6C/HgLUBHi8YhHlCEulrmQMntVl4PFdh3kA0sWyAY=
|
||||||
|
github.com/gotk3/gotk3 v0.5.3-0.20210223154815-289cfb6dbf32/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
|
github.com/joshuarubin/go-sway v0.0.3 h1:uuY+dAMz+iAJvso+DP7TSRczDWhaV47nEPHJoRDOqjA=
|
||||||
|
github.com/joshuarubin/go-sway v0.0.3/go.mod h1:qcDd6f25vJ0++wICwA1BainIcRC67p2Mb4lsrZ0k3/k=
|
||||||
|
github.com/joshuarubin/go-sway v0.0.4 h1:dpmIwQ/LytG+oMrjmaVKdk1aPdW2feXK/+wAcLKIx4A=
|
||||||
|
github.com/joshuarubin/go-sway v0.0.4/go.mod h1:qcDd6f25vJ0++wICwA1BainIcRC67p2Mb4lsrZ0k3/k=
|
||||||
|
github.com/joshuarubin/lifecycle v1.0.0 h1:N/lPEC8f+dBZ1Tn99vShqp36LwB+LI7XNAiNadZeLUQ=
|
||||||
|
github.com/joshuarubin/lifecycle v1.0.0/go.mod h1:sRy++ATvR9Ee21tkRdFkQeywAWvDsue66V70K0Dnl54=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||||
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
golang.org/x/sync v0.0.0-20190412183630-56d357773e84 h1:IqXQ59gzdXv58Jmm2xn0tSOR9i6HqroaOFRQ3wR/dJQ=
|
||||||
|
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
366
main.go
Normal file
366
main.go
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/allan-simon/go-singleinstance"
|
||||||
|
"github.com/dlasky/gotk3-layershell/layershell"
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
"github.com/gotk3/gotk3/glib"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
const version = "0.0.3"
|
||||||
|
|
||||||
|
var (
|
||||||
|
appDirs []string
|
||||||
|
configDirectory string
|
||||||
|
pinnedFile string
|
||||||
|
pinned []string
|
||||||
|
leftBox *gtk.Box
|
||||||
|
rightBox *gtk.Box
|
||||||
|
src glib.SourceHandle
|
||||||
|
imgSizeScaled int
|
||||||
|
currentWsNum, targetWsNum int64
|
||||||
|
win *gtk.Window
|
||||||
|
id2entry map[string]desktopEntry
|
||||||
|
)
|
||||||
|
|
||||||
|
var categoryNames = [...]string{
|
||||||
|
"utility",
|
||||||
|
"development",
|
||||||
|
"game",
|
||||||
|
"graphics",
|
||||||
|
"internet-and-network",
|
||||||
|
"office",
|
||||||
|
"audio-video",
|
||||||
|
"system-tools",
|
||||||
|
"other",
|
||||||
|
}
|
||||||
|
|
||||||
|
type category struct {
|
||||||
|
Name string
|
||||||
|
DisplayName string
|
||||||
|
Icon string
|
||||||
|
}
|
||||||
|
|
||||||
|
var categories []category
|
||||||
|
|
||||||
|
type desktopEntry struct {
|
||||||
|
DesktopID string
|
||||||
|
Name string
|
||||||
|
NameLoc string
|
||||||
|
Comment string
|
||||||
|
CommentLoc string
|
||||||
|
Icon string
|
||||||
|
Exec string
|
||||||
|
Terminal bool
|
||||||
|
NoDisplay bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// slices below will hold DesktopID strings
|
||||||
|
var (
|
||||||
|
listUtility []string
|
||||||
|
listDevelopment []string
|
||||||
|
listGame []string
|
||||||
|
listGraphics []string
|
||||||
|
listInternetAndNetwork []string
|
||||||
|
listOffice []string
|
||||||
|
listAudioVideo []string
|
||||||
|
listSystemTools []string
|
||||||
|
listOther []string
|
||||||
|
)
|
||||||
|
|
||||||
|
var desktopEntries []desktopEntry
|
||||||
|
|
||||||
|
// UI elements
|
||||||
|
var (
|
||||||
|
categoriesListBox *gtk.ListBox
|
||||||
|
userDirsListBox *gtk.ListBox
|
||||||
|
pinnedListBox *gtk.ListBox
|
||||||
|
resultWrapper *gtk.Box
|
||||||
|
resultWindow *gtk.ScrolledWindow
|
||||||
|
fileSearchResults map[string]string
|
||||||
|
fileSearchResultWindow *gtk.ScrolledWindow
|
||||||
|
backButton *gtk.Box
|
||||||
|
searchEntry *gtk.SearchEntry
|
||||||
|
phrase string
|
||||||
|
resultListBox *gtk.ListBox
|
||||||
|
fileSearchResultListBox *gtk.ListBox
|
||||||
|
buttonsWrapper *gtk.Box
|
||||||
|
buttonBox *gtk.EventBox
|
||||||
|
confirmationBox *gtk.Box
|
||||||
|
userDirsMap map[string]string
|
||||||
|
)
|
||||||
|
|
||||||
|
// Flags
|
||||||
|
var cssFileName = flag.String("s", "menu-start.css", "Styling: css file name")
|
||||||
|
var targetOutput = flag.String("o", "", "name of the Output to display the menu on")
|
||||||
|
var displayVersion = flag.Bool("v", false, "display Version information")
|
||||||
|
var autohide = flag.Bool("d", false, "auto-hiDe: close window when left")
|
||||||
|
var valign = flag.String("va", "bottom", "Vertical Alignment: \"bottom\" or \"top\"")
|
||||||
|
var halign = flag.String("ha", "left", "Horizontal Alignment: \"left\" or \"right\"")
|
||||||
|
var marginTop = flag.Int("mt", 0, "Margin Top")
|
||||||
|
var marginLeft = flag.Int("ml", 0, "Margin Left")
|
||||||
|
var marginRight = flag.Int("mr", 0, "Margin Right")
|
||||||
|
var marginBottom = flag.Int("mb", 0, "Margin Bottom")
|
||||||
|
var iconSizeLarge = flag.Int("isl", 32, "Icon Size Large")
|
||||||
|
var iconSizeSmall = flag.Int("iss", 16, "Icon Size Small")
|
||||||
|
var itemPadding = flag.Uint("padding", 2, "vertical item padding")
|
||||||
|
var lang = flag.String("lang", "", "force lang, e.g. \"en\", \"pl\"")
|
||||||
|
var fileManager = flag.String("fm", "thunar", "File Manager")
|
||||||
|
var term = flag.String("term", "alacritty", "Terminal emulator")
|
||||||
|
var windowWidth = flag.Int("width", 0, "window width")
|
||||||
|
var windowHeigth = flag.Int("height", 0, "window height")
|
||||||
|
var cmdLock = flag.String("cmd-lock", "swaylock -f -c 000000", "screen lock command")
|
||||||
|
var cmdLogout = flag.String("cmd-logout", "swaymsg exit", "logout command")
|
||||||
|
var cmdRestart = flag.String("cmd-restart", "systemctl reboot", "reboot command")
|
||||||
|
var cmdShutdown = flag.String("cmd-shutdown", "systemctl -i poweroff", "shutdown command")
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
timeStart := time.Now()
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *displayVersion {
|
||||||
|
fmt.Printf("nwg-menu version %s\n", version)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gentle SIGTERM handler thanks to reiki4040 https://gist.github.com/reiki4040/be3705f307d3cd136e85
|
||||||
|
signalChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signalChan, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
s := <-signalChan
|
||||||
|
if s == syscall.SIGTERM {
|
||||||
|
println("SIGTERM received, bye bye!")
|
||||||
|
gtk.MainQuit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// We want the same key/mouse binding to turn the dock off: kill the running instance and exit.
|
||||||
|
lockFilePath := fmt.Sprintf("%s/nwg-menu.lock", tempDir())
|
||||||
|
lockFile, err := singleinstance.CreateLockFile(lockFilePath)
|
||||||
|
if err != nil {
|
||||||
|
pid, err := readTextFile(lockFilePath)
|
||||||
|
if err == nil {
|
||||||
|
i, err := strconv.Atoi(pid)
|
||||||
|
if err == nil {
|
||||||
|
/*if !*autohide {
|
||||||
|
println("Running instance found, sending SIGTERM and exiting...")
|
||||||
|
syscall.Kill(i, syscall.SIGTERM)
|
||||||
|
} else {
|
||||||
|
println("Already running")
|
||||||
|
}*/
|
||||||
|
println("Running instance found, sending SIGTERM and exiting...")
|
||||||
|
syscall.Kill(i, syscall.SIGTERM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
defer lockFile.Close()
|
||||||
|
|
||||||
|
// LANGUAGE
|
||||||
|
if *lang == "" && os.Getenv("LANG") != "" {
|
||||||
|
*lang = strings.Split(os.Getenv("LANG"), ".")[0]
|
||||||
|
}
|
||||||
|
println(fmt.Sprintf("lang: %s", *lang))
|
||||||
|
|
||||||
|
// ENVIRONMENT
|
||||||
|
configDirectory = configDir()
|
||||||
|
|
||||||
|
if !pathExists(filepath.Join(configDirectory, "menu-start.css")) {
|
||||||
|
copyFile("/usr/share/nwg-menu/menu-start.css", filepath.Join(configDirectory, "menu-start.css"))
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDirectory := cacheDir()
|
||||||
|
if cacheDirectory == "" {
|
||||||
|
log.Panic("Couldn't determine cache directory location")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DATA
|
||||||
|
pinnedFile = filepath.Join(cacheDirectory, "nwg-pin-cache")
|
||||||
|
pinned, err = loadTextFile(pinnedFile)
|
||||||
|
if err != nil {
|
||||||
|
pinned = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cssFile := filepath.Join(configDirectory, *cssFileName)
|
||||||
|
|
||||||
|
appDirs = getAppDirs()
|
||||||
|
|
||||||
|
setUpCategories()
|
||||||
|
|
||||||
|
desktopFiles := listDesktopFiles()
|
||||||
|
println(fmt.Sprintf("Found %v desktop files", len(desktopFiles)))
|
||||||
|
|
||||||
|
parseDesktopFiles(desktopFiles)
|
||||||
|
|
||||||
|
// USER INTERFACE
|
||||||
|
gtk.Init(nil)
|
||||||
|
|
||||||
|
cssProvider, _ := gtk.CssProviderNew()
|
||||||
|
|
||||||
|
err = cssProvider.LoadFromPath(cssFile)
|
||||||
|
if err != nil {
|
||||||
|
println(fmt.Sprintf("ERROR: %s css file not found or erroneous. Using GTK styling.", cssFile))
|
||||||
|
println(fmt.Sprintf(">>> %s", err))
|
||||||
|
} else {
|
||||||
|
println(fmt.Sprintf("Using style from %s", cssFile))
|
||||||
|
screen, _ := gdk.ScreenGetDefault()
|
||||||
|
gtk.AddProviderForScreen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Unable to create window:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layershell.InitForWindow(win)
|
||||||
|
|
||||||
|
var output2mon map[string]*gdk.Monitor
|
||||||
|
if *targetOutput != "" {
|
||||||
|
// We want to assign layershell to a monitor, but we only know the output name!
|
||||||
|
output2mon, err = mapOutputs()
|
||||||
|
if err == nil {
|
||||||
|
monitor := output2mon[*targetOutput]
|
||||||
|
layershell.SetMonitor(win, monitor)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *valign == "bottom" {
|
||||||
|
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_BOTTOM, true)
|
||||||
|
} else {
|
||||||
|
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_TOP, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *halign == "left" {
|
||||||
|
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_LEFT, true)
|
||||||
|
} else {
|
||||||
|
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_RIGHT, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
layershell.SetLayer(win, layershell.LAYER_SHELL_LAYER_TOP)
|
||||||
|
|
||||||
|
layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_TOP, *marginTop)
|
||||||
|
layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_LEFT, *marginLeft)
|
||||||
|
layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_RIGHT, *marginRight)
|
||||||
|
layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_BOTTOM, *marginBottom)
|
||||||
|
|
||||||
|
layershell.SetKeyboardMode(win, layershell.LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE)
|
||||||
|
|
||||||
|
win.Connect("destroy", func() {
|
||||||
|
gtk.MainQuit()
|
||||||
|
})
|
||||||
|
|
||||||
|
win.Connect("key-release-event", func(window *gtk.Window, event *gdk.Event) {
|
||||||
|
key := &gdk.EventKey{Event: event}
|
||||||
|
if key.KeyVal() == gdk.KEY_Escape {
|
||||||
|
s, _ := searchEntry.GetText()
|
||||||
|
if s != "" {
|
||||||
|
clearSearchResult()
|
||||||
|
searchEntry.GrabFocus()
|
||||||
|
searchEntry.SetText("")
|
||||||
|
} else {
|
||||||
|
if resultWindow == nil || !resultWindow.GetVisible() {
|
||||||
|
gtk.MainQuit()
|
||||||
|
} else {
|
||||||
|
clearSearchResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Close the window on leave, but not immediately, to avoid accidental closes
|
||||||
|
win.Connect("leave-notify-event", func() {
|
||||||
|
if *autohide {
|
||||||
|
src, err = glib.TimeoutAdd(uint(1000), func() bool {
|
||||||
|
gtk.MainQuit()
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
win.Connect("enter-notify-event", func() {
|
||||||
|
cancelClose()
|
||||||
|
})
|
||||||
|
|
||||||
|
outerBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
win.Add(outerBox)
|
||||||
|
|
||||||
|
alignmentBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
|
//alignmentBox.SetHomogeneous(true)
|
||||||
|
outerBox.PackStart(alignmentBox, true, true, 0)
|
||||||
|
|
||||||
|
leftBox, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
|
alignmentBox.PackStart(leftBox, false, false, 10)
|
||||||
|
|
||||||
|
leftColumn, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
leftBox.PackStart(leftColumn, false, false, 0)
|
||||||
|
|
||||||
|
searchEntry = setUpSearchEntry()
|
||||||
|
if *valign == "top" {
|
||||||
|
leftColumn.PackStart(searchEntry, false, false, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
pinnedListBox = setUpPinnedListBox()
|
||||||
|
leftColumn.PackStart(pinnedListBox, false, false, 10)
|
||||||
|
|
||||||
|
/*sep, _ := gtk.SeparatorNew(gtk.ORIENTATION_HORIZONTAL)
|
||||||
|
leftColumn.PackStart(sep, false, false, 10)*/
|
||||||
|
|
||||||
|
categoriesListBox = setUpCategoriesListBox()
|
||||||
|
leftColumn.PackStart(categoriesListBox, false, false, 10)
|
||||||
|
|
||||||
|
if *valign != "top" {
|
||||||
|
leftColumn.PackEnd(searchEntry, false, false, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
rightBox, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
|
alignmentBox.PackStart(rightBox, true, true, 10)
|
||||||
|
|
||||||
|
rightColumn, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
|
||||||
|
rightBox.PackStart(rightColumn, true, true, 0)
|
||||||
|
|
||||||
|
userDirsListBox = setUpUserDirsList()
|
||||||
|
rightColumn.PackStart(userDirsListBox, false, true, 10)
|
||||||
|
|
||||||
|
backButton = setUpBackButton()
|
||||||
|
rightColumn.PackStart(backButton, false, false, 10)
|
||||||
|
|
||||||
|
resultWrapper, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
rightColumn.PackStart(resultWrapper, true, true, 0)
|
||||||
|
|
||||||
|
buttonsWrapper, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
|
||||||
|
buttonBox = setUpButtonBox()
|
||||||
|
buttonsWrapper.PackStart(buttonBox, false, false, 10)
|
||||||
|
rightColumn.PackEnd(buttonsWrapper, false, true, 0)
|
||||||
|
|
||||||
|
//win.SetSizeRequest(0, *windowHeigth)
|
||||||
|
|
||||||
|
win.ShowAll()
|
||||||
|
|
||||||
|
backButton.Hide()
|
||||||
|
|
||||||
|
pinnedListBox.UnselectAll()
|
||||||
|
categoriesListBox.UnselectAll()
|
||||||
|
searchEntry.GrabFocus()
|
||||||
|
t := time.Now()
|
||||||
|
println(fmt.Sprintf("UI created in %v ms. Thanks for watching.", t.Sub(timeStart).Milliseconds()))
|
||||||
|
gtk.Main()
|
||||||
|
}
|
||||||
22
menu-start.css
Normal file
22
menu-start.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
window {
|
||||||
|
background-color: rgba (36, 47, 79, 0.92);
|
||||||
|
color: #eeeeee
|
||||||
|
}
|
||||||
|
|
||||||
|
list {
|
||||||
|
background: none;
|
||||||
|
border-radius: 15px
|
||||||
|
}
|
||||||
|
|
||||||
|
entry {
|
||||||
|
background-color: rgba (0, 0, 0, 0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: none;
|
||||||
|
border: none
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: rgba (255, 255, 255, 0.1)
|
||||||
|
}
|
||||||
683
tools.go
Normal file
683
tools.go
Normal file
@@ -0,0 +1,683 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
"github.com/gotk3/gotk3/glib"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
"github.com/joshuarubin/go-sway"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Window on-leave-notify event hides the window with glib Timeout 1000 ms.
|
||||||
|
We might have left the window by accident, so let's clear the timeout if window re-entered.
|
||||||
|
Furthermore - hovering a widget triggers window on-leave-notify event, and the timeout
|
||||||
|
needs to be cleared as well.
|
||||||
|
*/
|
||||||
|
func cancelClose() {
|
||||||
|
if src > 0 {
|
||||||
|
glib.SourceRemove(src)
|
||||||
|
src = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func inPinned(taskID string) bool {
|
||||||
|
for _, id := range pinned {
|
||||||
|
if strings.TrimSpace(taskID) == strings.TrimSpace(id) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func createPixbuf(icon string, size int) (*gdk.Pixbuf, error) {
|
||||||
|
iconTheme, err := gtk.IconThemeGetDefault()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Couldn't get default theme: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(icon, "/") {
|
||||||
|
pixbuf, err := gdk.PixbufNewFromFileAtSize(icon, size, size)
|
||||||
|
if err != nil {
|
||||||
|
println(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pixbuf, nil
|
||||||
|
|
||||||
|
} else if strings.HasSuffix(icon, ".svg") || strings.HasSuffix(icon, ".png") || strings.HasSuffix(icon, ".xpm") {
|
||||||
|
// for enties like "Icon=netflix-desktop.svg"
|
||||||
|
icon = strings.Split(icon, ".")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
pixbuf, err := iconTheme.LoadIcon(icon, size, gtk.ICON_LOOKUP_FORCE_SIZE)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if strings.HasPrefix(icon, "/") {
|
||||||
|
pixbuf, err := gdk.PixbufNewFromFileAtSize(icon, size, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pixbuf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pixbuf, err := iconTheme.LoadIcon(icon, size, gtk.ICON_LOOKUP_FORCE_SIZE)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pixbuf, nil
|
||||||
|
}
|
||||||
|
return pixbuf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapXdgUserDirs() map[string]string {
|
||||||
|
result := make(map[string]string)
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
|
||||||
|
result["home"] = home
|
||||||
|
result["documents"] = filepath.Join(home, "Documents")
|
||||||
|
result["downloads"] = filepath.Join(home, "Downloads")
|
||||||
|
result["music"] = filepath.Join(home, "Music")
|
||||||
|
result["pictures"] = filepath.Join(home, "Pictures")
|
||||||
|
result["videos"] = filepath.Join(home, "Videos")
|
||||||
|
|
||||||
|
userDirsFile := filepath.Join(home, ".config/user-dirs.dirs")
|
||||||
|
if pathExists(userDirsFile) {
|
||||||
|
println(fmt.Sprintf("Using XDG user dirs from %s", userDirsFile))
|
||||||
|
lines, _ := loadTextFile(userDirsFile)
|
||||||
|
for _, l := range lines {
|
||||||
|
if strings.HasPrefix(l, "XDG_DOCUMENTS_DIR") {
|
||||||
|
result["documents"] = getUserDir(home, l)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, "XDG_DOWNLOAD_DIR") {
|
||||||
|
result["downloads"] = getUserDir(home, l)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, "XDG_MUSIC_DIR") {
|
||||||
|
result["music"] = getUserDir(home, l)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, "XDG_PICTURES_DIR") {
|
||||||
|
result["pictures"] = getUserDir(home, l)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, "XDG_VIDEOS_DIR") {
|
||||||
|
result["videos"] = getUserDir(home, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println(fmt.Sprintf("%s file not found, using defaults", userDirsFile))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserDir(home, line string) string {
|
||||||
|
// line is supposed to look like XDG_DOCUMENTS_DIR="$HOME/Dokumenty"
|
||||||
|
result := strings.Split(line, "=")[1]
|
||||||
|
result = strings.Replace(result, "$HOME", home, 1)
|
||||||
|
|
||||||
|
// trim ""
|
||||||
|
return result[1 : len(result)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheDir() string {
|
||||||
|
if os.Getenv("XDG_CACHE_HOME") != "" {
|
||||||
|
return os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
}
|
||||||
|
if os.Getenv("HOME") != "" && pathExists(filepath.Join(os.Getenv("HOME"), ".cache")) {
|
||||||
|
p := filepath.Join(os.Getenv("HOME"), ".cache")
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func tempDir() string {
|
||||||
|
if os.Getenv("TMPDIR") != "" {
|
||||||
|
return os.Getenv("TMPDIR")
|
||||||
|
} else if os.Getenv("TEMP") != "" {
|
||||||
|
return os.Getenv("TEMP")
|
||||||
|
} else if os.Getenv("TMP") != "" {
|
||||||
|
return os.Getenv("TMP")
|
||||||
|
}
|
||||||
|
return "/tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
func readTextFile(path string) (string, error) {
|
||||||
|
bytes, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func configDir() string {
|
||||||
|
if os.Getenv("XDG_CONFIG_HOME") != "" {
|
||||||
|
dir := fmt.Sprintf("%s/nwg-panel", os.Getenv("XDG_CONFIG_HOME"))
|
||||||
|
createDir(dir)
|
||||||
|
return (fmt.Sprintf("%s/nwg-panel", os.Getenv("XDG_CONFIG_HOME")))
|
||||||
|
}
|
||||||
|
dir := fmt.Sprintf("%s/.config/nwg-panel", os.Getenv("HOME"))
|
||||||
|
createDir(dir)
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDir(dir string) {
|
||||||
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
|
err := os.MkdirAll(dir, os.ModePerm)
|
||||||
|
if err == nil {
|
||||||
|
fmt.Println("Creating dir:", dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
fmt.Println("Copying file:", dst)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var srcfd *os.File
|
||||||
|
var dstfd *os.File
|
||||||
|
var srcinfo os.FileInfo
|
||||||
|
|
||||||
|
if srcfd, err = os.Open(src); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer srcfd.Close()
|
||||||
|
|
||||||
|
if dstfd, err = os.Create(dst); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dstfd.Close()
|
||||||
|
|
||||||
|
if _, err = io.Copy(dstfd, srcfd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if srcinfo, err = os.Stat(src); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Chmod(dst, srcinfo.Mode())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAppDirs() []string {
|
||||||
|
var dirs []string
|
||||||
|
xdgDataDirs := ""
|
||||||
|
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
xdgDataHome := os.Getenv("XDG_DATA_HOME")
|
||||||
|
if os.Getenv("XDG_DATA_DIRS") != "" {
|
||||||
|
xdgDataDirs = os.Getenv("XDG_DATA_DIRS")
|
||||||
|
} else {
|
||||||
|
xdgDataDirs = "/usr/local/share/:/usr/share/"
|
||||||
|
}
|
||||||
|
if xdgDataHome != "" {
|
||||||
|
dirs = append(dirs, filepath.Join(xdgDataHome, "applications"))
|
||||||
|
} else if home != "" {
|
||||||
|
dirs = append(dirs, filepath.Join(home, ".local/share/applications"))
|
||||||
|
}
|
||||||
|
for _, d := range strings.Split(xdgDataDirs, ":") {
|
||||||
|
dirs = append(dirs, filepath.Join(d, "applications"))
|
||||||
|
}
|
||||||
|
flatpakDirs := []string{filepath.Join(home, ".local/share/flatpak/exports/share/applications"),
|
||||||
|
"/var/lib/flatpak/exports/share/applications"}
|
||||||
|
|
||||||
|
for _, d := range flatpakDirs {
|
||||||
|
if !isIn(dirs, d) {
|
||||||
|
dirs = append(dirs, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
func listFiles(dir string) ([]fs.FileInfo, error) {
|
||||||
|
files, err := ioutil.ReadDir(dir)
|
||||||
|
if err == nil {
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func listDesktopFiles() []string {
|
||||||
|
var paths []string
|
||||||
|
for _, dir := range appDirs {
|
||||||
|
dirs, err := listFiles(dir)
|
||||||
|
if err == nil {
|
||||||
|
for _, file := range dirs {
|
||||||
|
parts := strings.Split(file.Name(), ".")
|
||||||
|
if parts[len(parts)-1] == "desktop" {
|
||||||
|
paths = append(paths, filepath.Join(dir, file.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUpCategories() {
|
||||||
|
path := "/usr/share/nwg-menu/desktop-directories"
|
||||||
|
var other category
|
||||||
|
|
||||||
|
for _, cName := range categoryNames {
|
||||||
|
fileName := fmt.Sprintf("%s.directory", cName)
|
||||||
|
lines, err := loadTextFile(filepath.Join(path, fileName))
|
||||||
|
if err == nil {
|
||||||
|
var cat category
|
||||||
|
cat.Name = cName
|
||||||
|
|
||||||
|
name := ""
|
||||||
|
nameLoc := ""
|
||||||
|
icon := ""
|
||||||
|
|
||||||
|
for _, l := range lines {
|
||||||
|
if strings.HasPrefix(l, "Name=") {
|
||||||
|
name = strings.Split(l, "=")[1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, fmt.Sprintf("Name[%s]=", strings.Split(*lang, "_")[0])) {
|
||||||
|
nameLoc = strings.Split(l, "=")[1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, "Icon=") {
|
||||||
|
icon = strings.Split(l, "=")[1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nameLoc == "" {
|
||||||
|
for _, l := range lines {
|
||||||
|
if strings.HasPrefix(l, fmt.Sprintf("Name[%s]=", *lang)) {
|
||||||
|
nameLoc = strings.Split(l, "=")[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nameLoc != "" {
|
||||||
|
cat.DisplayName = nameLoc
|
||||||
|
} else {
|
||||||
|
cat.DisplayName = name
|
||||||
|
}
|
||||||
|
cat.Icon = icon
|
||||||
|
|
||||||
|
// We want "other" to be the last one. Let's append it when already sorted
|
||||||
|
if fileName != "other.directory" {
|
||||||
|
categories = append(categories, cat)
|
||||||
|
} else {
|
||||||
|
other = cat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(categories, func(i, j int) bool {
|
||||||
|
return categories[i].DisplayName < categories[j].DisplayName
|
||||||
|
})
|
||||||
|
categories = append(categories, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDesktopFiles(desktopFiles []string) {
|
||||||
|
id2entry = make(map[string]desktopEntry)
|
||||||
|
var added []string
|
||||||
|
skipped := 0
|
||||||
|
hidden := 0
|
||||||
|
for _, file := range desktopFiles {
|
||||||
|
lines, err := loadTextFile(file)
|
||||||
|
if err == nil {
|
||||||
|
parts := strings.Split(file, "/")
|
||||||
|
desktopID := parts[len(parts)-1]
|
||||||
|
name := ""
|
||||||
|
nameLoc := ""
|
||||||
|
comment := ""
|
||||||
|
commentLoc := ""
|
||||||
|
icon := ""
|
||||||
|
exec := ""
|
||||||
|
terminal := false
|
||||||
|
noDisplay := false
|
||||||
|
|
||||||
|
categories := ""
|
||||||
|
|
||||||
|
for _, l := range lines {
|
||||||
|
if strings.HasPrefix(l, "[") && l != "[Desktop Entry]" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, "Name=") {
|
||||||
|
name = strings.Split(l, "=")[1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, fmt.Sprintf("Name[%s]=", strings.Split(*lang, "_")[0])) {
|
||||||
|
nameLoc = strings.Split(l, "=")[1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, "Comment=") {
|
||||||
|
comment = strings.Split(l, "=")[1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, fmt.Sprintf("Comment[%s]=", strings.Split(*lang, "_")[0])) {
|
||||||
|
commentLoc = strings.Split(l, "=")[1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, "Icon=") {
|
||||||
|
icon = strings.Split(l, "=")[1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, "Exec=") {
|
||||||
|
exec = strings.Split(l, "Exec=")[1]
|
||||||
|
disallowed := [2]string{"\"", "'"}
|
||||||
|
for _, char := range disallowed {
|
||||||
|
if strings.Contains(exec, char) {
|
||||||
|
exec = strings.Replace(exec, char, "", -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, "Categories=") {
|
||||||
|
categories = strings.Split(l, "Categories=")[1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if l == "Terminal=true" {
|
||||||
|
terminal = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if l == "NoDisplay=true" {
|
||||||
|
noDisplay = true
|
||||||
|
hidden++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if name[ln] not found, let's try to find name[ln_LN]
|
||||||
|
if nameLoc == "" {
|
||||||
|
nameLoc = name
|
||||||
|
}
|
||||||
|
if commentLoc == "" {
|
||||||
|
commentLoc = comment
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isIn(added, desktopID) {
|
||||||
|
added = append(added, desktopID)
|
||||||
|
|
||||||
|
var entry desktopEntry
|
||||||
|
entry.DesktopID = desktopID
|
||||||
|
entry.Name = name
|
||||||
|
entry.NameLoc = nameLoc
|
||||||
|
entry.Comment = comment
|
||||||
|
entry.CommentLoc = commentLoc
|
||||||
|
entry.Icon = icon
|
||||||
|
entry.Exec = exec
|
||||||
|
entry.Terminal = terminal
|
||||||
|
entry.NoDisplay = noDisplay
|
||||||
|
desktopEntries = append(desktopEntries, entry)
|
||||||
|
|
||||||
|
id2entry[entry.DesktopID] = entry
|
||||||
|
|
||||||
|
assignToLists(entry.DesktopID, categories)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
skipped++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println(fmt.Sprintf("Skipped %v duplicates; %v .desktop entries hidden by \"NoDisplay=true\"", skipped, hidden))
|
||||||
|
}
|
||||||
|
|
||||||
|
// freedesktop Main Categories list consists of 13 entries. Let's contract it to 8+1 ("Other").
|
||||||
|
func assignToLists(desktopID, categories string) {
|
||||||
|
cats := strings.Split(categories, ";")
|
||||||
|
assigned := false
|
||||||
|
for _, cat := range cats {
|
||||||
|
if cat == "Utility" && !isIn(listUtility, desktopID) {
|
||||||
|
listUtility = append(listUtility, desktopID)
|
||||||
|
assigned = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cat == "Development" && !isIn(listDevelopment, desktopID) {
|
||||||
|
listDevelopment = append(listDevelopment, desktopID)
|
||||||
|
assigned = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cat == "Game" && !isIn(listGame, desktopID) {
|
||||||
|
listGame = append(listGame, desktopID)
|
||||||
|
assigned = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cat == "Graphics" && !isIn(listGraphics, desktopID) {
|
||||||
|
listGraphics = append(listGraphics, desktopID)
|
||||||
|
assigned = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cat == "Network" && !isIn(listInternetAndNetwork, desktopID) {
|
||||||
|
listInternetAndNetwork = append(listInternetAndNetwork, desktopID)
|
||||||
|
assigned = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isIn([]string{"Office", "Science", "Education"}, cat) && !isIn(listOffice, desktopID) {
|
||||||
|
listOffice = append(listOffice, desktopID)
|
||||||
|
assigned = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isIn([]string{"AudioVideo", "Audio", "Video"}, cat) && !isIn(listAudioVideo, desktopID) {
|
||||||
|
listAudioVideo = append(listAudioVideo, desktopID)
|
||||||
|
assigned = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isIn([]string{"Settings", "System", "DesktopSettings", "PackageManager"}, cat) && !isIn(listSystemTools, desktopID) {
|
||||||
|
listSystemTools = append(listSystemTools, desktopID)
|
||||||
|
assigned = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if categories != "" && !assigned && !isIn(listOther, desktopID) {
|
||||||
|
listOther = append(listOther, desktopID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIn(slice []string, val string) bool {
|
||||||
|
for _, item := range slice {
|
||||||
|
if item == val {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathExists(name string) bool {
|
||||||
|
if _, err := os.Stat(name); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTextFile(path string) ([]string, error) {
|
||||||
|
bytes, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(bytes), "\n")
|
||||||
|
var output []string
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line != "" {
|
||||||
|
output = append(output, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pinItem(itemID string) {
|
||||||
|
for _, item := range pinned {
|
||||||
|
if item == itemID {
|
||||||
|
println(item, "already pinned")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pinned = append(pinned, itemID)
|
||||||
|
savePinned()
|
||||||
|
println(itemID, "pinned")
|
||||||
|
|
||||||
|
row := setUpPinnedListBoxRow(itemID)
|
||||||
|
pinnedListBox.Add(row)
|
||||||
|
pinnedListBox.ShowAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpinItem(itemID string) {
|
||||||
|
if isIn(pinned, itemID) {
|
||||||
|
pinned = remove(pinned, itemID)
|
||||||
|
savePinned()
|
||||||
|
println(itemID, "unpinned")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(s []string, r string) []string {
|
||||||
|
for i, v := range s {
|
||||||
|
if v == r {
|
||||||
|
return append(s[:i], s[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func savePinned() {
|
||||||
|
f, err := os.OpenFile(pinnedFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
for _, line := range pinned {
|
||||||
|
if line != "" {
|
||||||
|
_, err := f.WriteString(line + "\n")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
println("Error saving pinned", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func launch(command string, terminal bool) {
|
||||||
|
// trim % and everything afterwards
|
||||||
|
if strings.Contains(command, "%") {
|
||||||
|
cutAt := strings.Index(command, "%")
|
||||||
|
if cutAt != -1 {
|
||||||
|
command = command[:cutAt-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elements := strings.Split(command, " ")
|
||||||
|
|
||||||
|
// find prepended env variables, if any
|
||||||
|
envVarsNum := strings.Count(command, "=")
|
||||||
|
var envVars []string
|
||||||
|
|
||||||
|
cmdIdx := 0
|
||||||
|
lastEnvVarIdx := 0
|
||||||
|
|
||||||
|
if envVarsNum > 0 {
|
||||||
|
for idx, item := range elements {
|
||||||
|
if strings.Contains(item, "=") {
|
||||||
|
lastEnvVarIdx = idx
|
||||||
|
envVars = append(envVars, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmdIdx = lastEnvVarIdx + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(elements[cmdIdx], elements[1+cmdIdx:]...)
|
||||||
|
|
||||||
|
if terminal {
|
||||||
|
args := []string{"-e", elements[cmdIdx]}
|
||||||
|
cmd = exec.Command(*term, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set env variables
|
||||||
|
if len(envVars) > 0 {
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
for _, envVar := range envVars {
|
||||||
|
cmd.Env = append(cmd.Env, envVar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("env vars: %s; command: '%s'; args: %s\n", envVars, elements[cmdIdx], elements[1+cmdIdx:])
|
||||||
|
println(msg)
|
||||||
|
|
||||||
|
go cmd.Run()
|
||||||
|
|
||||||
|
glib.TimeoutAdd(uint(150), func() bool {
|
||||||
|
gtk.MainQuit()
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func open(filePath string) {
|
||||||
|
cmd := exec.Command(*fileManager, filePath)
|
||||||
|
go cmd.Run()
|
||||||
|
|
||||||
|
glib.TimeoutAdd(uint(150), func() bool {
|
||||||
|
gtk.MainQuit()
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns map output name -> gdk.Monitor
|
||||||
|
func mapOutputs() (map[string]*gdk.Monitor, error) {
|
||||||
|
result := make(map[string]*gdk.Monitor)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client, err := sway.New(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs, err := client.GetOutputs(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
display, err := gdk.DisplayGetDefault()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
num := display.GetNMonitors()
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
monitor, _ := display.GetMonitor(i)
|
||||||
|
geometry := monitor.GetGeometry()
|
||||||
|
// assign output to monitor on the basis of the same x, y coordinates
|
||||||
|
for _, output := range outputs {
|
||||||
|
if int(output.Rect.X) == geometry.GetX() && int(output.Rect.Y) == geometry.GetY() {
|
||||||
|
result[output.Name] = monitor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listMonitors() ([]gdk.Monitor, error) {
|
||||||
|
var monitors []gdk.Monitor
|
||||||
|
display, err := gdk.DisplayGetDefault()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
num := display.GetNMonitors()
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
monitor, _ := display.GetMonitor(i)
|
||||||
|
monitors = append(monitors, *monitor)
|
||||||
|
}
|
||||||
|
return monitors, nil
|
||||||
|
}
|
||||||
682
uicomponents.go
Normal file
682
uicomponents.go
Normal file
@@ -0,0 +1,682 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setUpPinnedListBox() *gtk.ListBox {
|
||||||
|
listBox, _ := gtk.ListBoxNew()
|
||||||
|
|
||||||
|
if len(pinned) > 0 {
|
||||||
|
for _, desktopID := range pinned {
|
||||||
|
row := setUpPinnedListBoxRow(desktopID)
|
||||||
|
listBox.Add(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listBox.Connect("enter-notify-event", func() {
|
||||||
|
cancelClose()
|
||||||
|
restoreButtonBox()
|
||||||
|
})
|
||||||
|
|
||||||
|
return listBox
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUpPinnedListBoxRow(desktopID string) *gtk.ListBoxRow {
|
||||||
|
entry := id2entry[desktopID]
|
||||||
|
|
||||||
|
row, _ := gtk.ListBoxRowNew()
|
||||||
|
row.SetSelectable(false)
|
||||||
|
row.SetCanFocus(false)
|
||||||
|
vBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
|
||||||
|
// We need gtk.EventBox to detect mouse event
|
||||||
|
eventBox, _ := gtk.EventBoxNew()
|
||||||
|
hBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 6)
|
||||||
|
eventBox.Add(hBox)
|
||||||
|
vBox.PackStart(eventBox, false, false, *itemPadding)
|
||||||
|
|
||||||
|
pixbuf, _ := createPixbuf(entry.Icon, *iconSizeLarge)
|
||||||
|
img, err := gtk.ImageNewFromPixbuf(pixbuf)
|
||||||
|
if err != nil {
|
||||||
|
println(err, entry.Icon)
|
||||||
|
}
|
||||||
|
hBox.PackStart(img, false, false, 0)
|
||||||
|
lbl, _ := gtk.LabelNew("")
|
||||||
|
name := ""
|
||||||
|
if entry.NameLoc != "" {
|
||||||
|
name = entry.NameLoc
|
||||||
|
} else {
|
||||||
|
name = entry.Name
|
||||||
|
}
|
||||||
|
if len(name) > 35 {
|
||||||
|
name = fmt.Sprintf("%s...", name[:32])
|
||||||
|
}
|
||||||
|
lbl.SetText(name)
|
||||||
|
hBox.PackStart(lbl, false, false, 0)
|
||||||
|
row.Add(vBox)
|
||||||
|
|
||||||
|
row.Connect("activate", func() {
|
||||||
|
launch(entry.Exec, entry.Terminal)
|
||||||
|
})
|
||||||
|
|
||||||
|
eventBox.Connect("button-release-event", func(row *gtk.ListBoxRow, e *gdk.Event) bool {
|
||||||
|
btnEvent := gdk.EventButtonNewFromEvent(e)
|
||||||
|
if btnEvent.Button() == 1 {
|
||||||
|
launch(entry.Exec, entry.Terminal)
|
||||||
|
return true
|
||||||
|
} else if btnEvent.Button() == 3 {
|
||||||
|
unpinItem(entry.DesktopID)
|
||||||
|
row.Destroy()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUpCategoriesListBox() *gtk.ListBox {
|
||||||
|
listBox, _ := gtk.ListBoxNew()
|
||||||
|
for _, cat := range categories {
|
||||||
|
if isSupposedToShowUp(cat.Name) {
|
||||||
|
row, _ := gtk.ListBoxRowNew()
|
||||||
|
row.SetCanFocus(false)
|
||||||
|
row.SetSelectable(false)
|
||||||
|
vBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
eventBox, _ := gtk.EventBoxNew()
|
||||||
|
hBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 6)
|
||||||
|
eventBox.Add(hBox)
|
||||||
|
vBox.PackStart(eventBox, false, false, *itemPadding)
|
||||||
|
|
||||||
|
connectCategoryListBox(cat.Name, eventBox, row)
|
||||||
|
|
||||||
|
pixbuf, _ := createPixbuf(cat.Icon, *iconSizeLarge)
|
||||||
|
img, _ := gtk.ImageNewFromPixbuf(pixbuf)
|
||||||
|
hBox.PackStart(img, false, false, 0)
|
||||||
|
|
||||||
|
lbl, _ := gtk.LabelNew(cat.DisplayName)
|
||||||
|
hBox.PackStart(lbl, false, false, 0)
|
||||||
|
|
||||||
|
pixbuf, _ = createPixbuf("pan-end-symbolic", *iconSizeSmall)
|
||||||
|
img, _ = gtk.ImageNewFromPixbuf(pixbuf)
|
||||||
|
hBox.PackEnd(img, false, false, 0)
|
||||||
|
|
||||||
|
row.Add(vBox)
|
||||||
|
listBox.Add(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listBox.Connect("enter-notify-event", func() {
|
||||||
|
cancelClose()
|
||||||
|
restoreButtonBox()
|
||||||
|
})
|
||||||
|
return listBox
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSupposedToShowUp(catName string) bool {
|
||||||
|
result := catName == "utility" && notEmpty(listUtility) ||
|
||||||
|
catName == "development" && notEmpty(listDevelopment) ||
|
||||||
|
catName == "game" && notEmpty(listGame) ||
|
||||||
|
catName == "graphics" && notEmpty(listGraphics) ||
|
||||||
|
catName == "internet-and-network" && notEmpty(listInternetAndNetwork) ||
|
||||||
|
catName == "office" && notEmpty(listOffice) ||
|
||||||
|
catName == "audio-video" && notEmpty(listAudioVideo) ||
|
||||||
|
catName == "system-tools" && notEmpty(listSystemTools) ||
|
||||||
|
catName == "other" && notEmpty(listOther)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func notEmpty(listCategory []string) bool {
|
||||||
|
if len(listCategory) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, desktopID := range listCategory {
|
||||||
|
entry := id2entry[desktopID]
|
||||||
|
if entry.NoDisplay == false {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectCategoryListBox(catName string, eventBox *gtk.EventBox, row *gtk.ListBoxRow) {
|
||||||
|
var listCategory []string
|
||||||
|
|
||||||
|
switch catName {
|
||||||
|
case "utility":
|
||||||
|
listCategory = listUtility
|
||||||
|
case "development":
|
||||||
|
listCategory = listDevelopment
|
||||||
|
case "game":
|
||||||
|
listCategory = listGame
|
||||||
|
case "graphics":
|
||||||
|
listCategory = listGraphics
|
||||||
|
case "internet-and-network":
|
||||||
|
listCategory = listInternetAndNetwork
|
||||||
|
case "office":
|
||||||
|
listCategory = listOffice
|
||||||
|
case "audio-video":
|
||||||
|
listCategory = listAudioVideo
|
||||||
|
case "system-tools":
|
||||||
|
listCategory = listSystemTools
|
||||||
|
default:
|
||||||
|
listCategory = listOther
|
||||||
|
}
|
||||||
|
|
||||||
|
eventBox.Connect("button-release-event", func(eb *gtk.EventBox, e *gdk.Event) bool {
|
||||||
|
btnEvent := gdk.EventButtonNewFromEvent(e)
|
||||||
|
if btnEvent.Button() == 1 {
|
||||||
|
searchEntry.SetText("")
|
||||||
|
clearSearchResult()
|
||||||
|
row.SetSelectable(true)
|
||||||
|
row.SetCanFocus(false)
|
||||||
|
categoriesListBox.SelectRow(row)
|
||||||
|
listBox := setUpCategoryListBox(listCategory)
|
||||||
|
if resultWindow != nil {
|
||||||
|
resultWindow.Destroy()
|
||||||
|
}
|
||||||
|
resultWindow, _ = gtk.ScrolledWindowNew(nil, nil)
|
||||||
|
resultWindow.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||||
|
resultWindow.Connect("enter-notify-event", func() {
|
||||||
|
cancelClose()
|
||||||
|
})
|
||||||
|
resultWrapper.PackStart(resultWindow, true, true, 0)
|
||||||
|
resultWindow.Add(listBox)
|
||||||
|
|
||||||
|
userDirsListBox.Hide()
|
||||||
|
resultWindow.ShowAll()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUpBackButton() *gtk.Box {
|
||||||
|
vBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
hBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 10)
|
||||||
|
vBox.PackStart(hBox, false, false, 0)
|
||||||
|
button, _ := gtk.ButtonNew()
|
||||||
|
button.SetCanFocus(false)
|
||||||
|
pixbuf, _ := createPixbuf("arrow-left", *iconSizeLarge)
|
||||||
|
image, _ := gtk.ImageNewFromPixbuf(pixbuf)
|
||||||
|
button.SetImage(image)
|
||||||
|
button.SetAlwaysShowImage(true)
|
||||||
|
button.Connect("enter-notify-event", func() {
|
||||||
|
cancelClose()
|
||||||
|
})
|
||||||
|
button.Connect("clicked", func(btn *gtk.Button) {
|
||||||
|
clearSearchResult()
|
||||||
|
searchEntry.GrabFocus()
|
||||||
|
searchEntry.SetText("")
|
||||||
|
})
|
||||||
|
hBox.PackEnd(button, false, true, 0)
|
||||||
|
|
||||||
|
/*sep, _ := gtk.SeparatorNew(gtk.ORIENTATION_HORIZONTAL)
|
||||||
|
sep.SetCanFocus(false)
|
||||||
|
vBox.Add(sep)*/
|
||||||
|
|
||||||
|
return vBox
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUpCategoryListBox(listCategory []string) *gtk.ListBox {
|
||||||
|
listBox, _ := gtk.ListBoxNew()
|
||||||
|
|
||||||
|
for _, desktopID := range listCategory {
|
||||||
|
entry := id2entry[desktopID]
|
||||||
|
name := entry.NameLoc
|
||||||
|
if name == "" {
|
||||||
|
name = entry.Name
|
||||||
|
}
|
||||||
|
if len(name) > 30 {
|
||||||
|
name = fmt.Sprintf("%s...", name[:27])
|
||||||
|
}
|
||||||
|
if !entry.NoDisplay {
|
||||||
|
row, _ := gtk.ListBoxRowNew()
|
||||||
|
row.SetSelectable(false)
|
||||||
|
vBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 10)
|
||||||
|
eventBox, _ := gtk.EventBoxNew()
|
||||||
|
hBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 6)
|
||||||
|
eventBox.Add(hBox)
|
||||||
|
vBox.PackStart(eventBox, false, false, *itemPadding)
|
||||||
|
|
||||||
|
ID := entry.DesktopID
|
||||||
|
eventBox.Connect("button-release-event", func(row *gtk.ListBoxRow, e *gdk.Event) bool {
|
||||||
|
btnEvent := gdk.EventButtonNewFromEvent(e)
|
||||||
|
if btnEvent.Button() == 1 {
|
||||||
|
launch(entry.Exec, entry.Terminal)
|
||||||
|
return true
|
||||||
|
} else if btnEvent.Button() == 3 {
|
||||||
|
pinItem(ID)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
pixbuf, _ := createPixbuf(entry.Icon, *iconSizeLarge)
|
||||||
|
img, _ := gtk.ImageNewFromPixbuf(pixbuf)
|
||||||
|
hBox.PackStart(img, false, false, 0)
|
||||||
|
|
||||||
|
lbl, _ := gtk.LabelNew(name)
|
||||||
|
hBox.PackStart(lbl, false, false, 0)
|
||||||
|
|
||||||
|
row.Add(vBox)
|
||||||
|
listBox.Add(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backButton.Show()
|
||||||
|
return listBox
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUpCategorySearchResult(searchPhrase string) *gtk.ListBox {
|
||||||
|
listBox, _ := gtk.ListBoxNew()
|
||||||
|
|
||||||
|
resultWindow, _ = gtk.ScrolledWindowNew(nil, nil)
|
||||||
|
resultWindow.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||||
|
resultWindow.Connect("enter-notify-event", func() {
|
||||||
|
cancelClose()
|
||||||
|
restoreButtonBox()
|
||||||
|
})
|
||||||
|
resultWrapper.PackStart(resultWindow, true, true, 0)
|
||||||
|
|
||||||
|
counter := 0
|
||||||
|
for _, entry := range desktopEntries {
|
||||||
|
if len(searchPhrase) == 1 && counter > 9 {
|
||||||
|
break
|
||||||
|
} else if len(searchPhrase) == 2 && counter > 14 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !entry.NoDisplay && (strings.Contains(strings.ToLower(entry.NameLoc), strings.ToLower(searchPhrase)) ||
|
||||||
|
strings.Contains(strings.ToLower(entry.CommentLoc), strings.ToLower(searchPhrase)) ||
|
||||||
|
strings.Contains(strings.ToLower(entry.Comment), strings.ToLower(searchPhrase))) {
|
||||||
|
|
||||||
|
counter++
|
||||||
|
|
||||||
|
row, _ := gtk.ListBoxRowNew()
|
||||||
|
row.SetSelectable(false)
|
||||||
|
vBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 10)
|
||||||
|
eventBox, _ := gtk.EventBoxNew()
|
||||||
|
hBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 6)
|
||||||
|
eventBox.Add(hBox)
|
||||||
|
vBox.PackStart(eventBox, false, false, *itemPadding)
|
||||||
|
|
||||||
|
exec := entry.Exec
|
||||||
|
term := entry.Terminal
|
||||||
|
ID := entry.DesktopID
|
||||||
|
row.Connect("activate", func() {
|
||||||
|
launch(exec, term)
|
||||||
|
})
|
||||||
|
eventBox.Connect("button-release-event", func(row *gtk.EventBox, e *gdk.Event) bool {
|
||||||
|
btnEvent := gdk.EventButtonNewFromEvent(e)
|
||||||
|
if btnEvent.Button() == 1 {
|
||||||
|
launch(exec, term)
|
||||||
|
return true
|
||||||
|
} else if btnEvent.Button() == 3 {
|
||||||
|
pinItem(ID)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
pixbuf, _ := createPixbuf(entry.Icon, *iconSizeLarge)
|
||||||
|
img, _ := gtk.ImageNewFromPixbuf(pixbuf)
|
||||||
|
hBox.PackStart(img, false, false, 0)
|
||||||
|
|
||||||
|
name := entry.NameLoc
|
||||||
|
if len(name) > 45 {
|
||||||
|
name = fmt.Sprintf("%s...", name[:42])
|
||||||
|
}
|
||||||
|
|
||||||
|
lbl, _ := gtk.LabelNew(name)
|
||||||
|
hBox.PackStart(lbl, false, false, 0)
|
||||||
|
|
||||||
|
row.Add(vBox)
|
||||||
|
listBox.Add(row)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultWindow.Add(listBox)
|
||||||
|
resultWindow.ShowAll()
|
||||||
|
return listBox
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUpFileSearchResult() *gtk.ListBox {
|
||||||
|
listBox, _ := gtk.ListBoxNew()
|
||||||
|
if fileSearchResultWindow != nil {
|
||||||
|
fileSearchResultWindow.Destroy()
|
||||||
|
}
|
||||||
|
fileSearchResultWindow, _ = gtk.ScrolledWindowNew(nil, nil)
|
||||||
|
fileSearchResultWindow.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||||
|
fileSearchResultWindow.Connect("enter-notify-event", func() {
|
||||||
|
cancelClose()
|
||||||
|
restoreButtonBox()
|
||||||
|
})
|
||||||
|
resultWrapper.PackStart(fileSearchResultWindow, true, true, 0)
|
||||||
|
|
||||||
|
fileSearchResultWindow.Add(listBox)
|
||||||
|
fileSearchResultWindow.ShowAll()
|
||||||
|
|
||||||
|
return listBox
|
||||||
|
}
|
||||||
|
|
||||||
|
func walk(path string, d fs.DirEntry, e error) error {
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if !d.IsDir() {
|
||||||
|
parts := strings.Split(path, "/")
|
||||||
|
fileName := parts[len(parts)-1]
|
||||||
|
if strings.Contains(strings.ToLower(fileName), strings.ToLower(phrase)) {
|
||||||
|
fileSearchResults[fileName] = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUpSearchEntry() *gtk.SearchEntry {
|
||||||
|
searchEntry, _ := gtk.SearchEntryNew()
|
||||||
|
searchEntry.Connect("enter-notify-event", func() {
|
||||||
|
cancelClose()
|
||||||
|
restoreButtonBox()
|
||||||
|
})
|
||||||
|
searchEntry.Connect("search-changed", func() {
|
||||||
|
phrase, _ = searchEntry.GetText()
|
||||||
|
if len(phrase) > 0 {
|
||||||
|
userDirsListBox.Hide()
|
||||||
|
backButton.Show()
|
||||||
|
|
||||||
|
if resultWindow != nil {
|
||||||
|
resultWindow.Destroy()
|
||||||
|
}
|
||||||
|
resultListBox = setUpCategorySearchResult(phrase)
|
||||||
|
if resultListBox.GetChildren().Length() == 0 {
|
||||||
|
resultWindow.Hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(phrase) > 2 {
|
||||||
|
if fileSearchResultWindow != nil {
|
||||||
|
fileSearchResultWindow.Destroy()
|
||||||
|
}
|
||||||
|
fileSearchResultListBox = setUpFileSearchResult()
|
||||||
|
for key := range userDirsMap {
|
||||||
|
if key != "home" {
|
||||||
|
fileSearchResults = make(map[string]string)
|
||||||
|
if len(fileSearchResults) == 0 {
|
||||||
|
fileSearchResultListBox.Show()
|
||||||
|
}
|
||||||
|
filepath.WalkDir(userDirsMap[key], walk)
|
||||||
|
searchUserDir(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fileSearchResultListBox.GetChildren().Length() == 0 {
|
||||||
|
fileSearchResultWindow.Hide()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if fileSearchResultWindow != nil {
|
||||||
|
fileSearchResultWindow.Destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
clearSearchResult()
|
||||||
|
userDirsListBox.ShowAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
searchEntry.Connect("focus-in-event", func() {
|
||||||
|
searchEntry.SetText("")
|
||||||
|
})
|
||||||
|
|
||||||
|
return searchEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchUserDir(dir string) {
|
||||||
|
fileSearchResults = make(map[string]string)
|
||||||
|
filepath.WalkDir(userDirsMap[dir], walk)
|
||||||
|
if len(fileSearchResults) > 0 {
|
||||||
|
row := setUpUserDirsListRow(fmt.Sprintf("folder-%s", dir), "", dir, userDirsMap)
|
||||||
|
fileSearchResultListBox.Add(row)
|
||||||
|
fileSearchResultListBox.ShowAll()
|
||||||
|
|
||||||
|
for fileName, path := range fileSearchResults {
|
||||||
|
row := setUpUserFileSearchResultRow(fileName, path)
|
||||||
|
fileSearchResultListBox.Add(row)
|
||||||
|
}
|
||||||
|
fileSearchResultListBox.ShowAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUpUserDirsList() *gtk.ListBox {
|
||||||
|
listBox, _ := gtk.ListBoxNew()
|
||||||
|
userDirsMap = mapXdgUserDirs()
|
||||||
|
|
||||||
|
row := setUpUserDirsListRow("folder-home", "Home", "home", userDirsMap)
|
||||||
|
listBox.Add(row)
|
||||||
|
row = setUpUserDirsListRow("folder-documents", "", "documents", userDirsMap)
|
||||||
|
listBox.Add(row)
|
||||||
|
row = setUpUserDirsListRow("folder-downloads", "", "downloads", userDirsMap)
|
||||||
|
listBox.Add(row)
|
||||||
|
row = setUpUserDirsListRow("folder-music", "", "music", userDirsMap)
|
||||||
|
listBox.Add(row)
|
||||||
|
row = setUpUserDirsListRow("folder-pictures", "", "pictures", userDirsMap)
|
||||||
|
listBox.Add(row)
|
||||||
|
row = setUpUserDirsListRow("folder-videos", "", "videos", userDirsMap)
|
||||||
|
listBox.Add(row)
|
||||||
|
|
||||||
|
listBox.Connect("enter-notify-event", func() {
|
||||||
|
cancelClose()
|
||||||
|
restoreButtonBox()
|
||||||
|
})
|
||||||
|
|
||||||
|
return listBox
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUpUserDirsListRow(iconName, displayName, entryName string, userDirsMap map[string]string) *gtk.ListBoxRow {
|
||||||
|
if displayName == "" {
|
||||||
|
parts := strings.Split(userDirsMap[entryName], "/")
|
||||||
|
displayName = parts[(len(parts) - 1)]
|
||||||
|
}
|
||||||
|
row, _ := gtk.ListBoxRowNew()
|
||||||
|
//row.SetCanFocus(false)
|
||||||
|
row.SetSelectable(false)
|
||||||
|
vBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
eventBox, _ := gtk.EventBoxNew()
|
||||||
|
hBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 6)
|
||||||
|
eventBox.Add(hBox)
|
||||||
|
vBox.PackStart(eventBox, false, false, *itemPadding*3)
|
||||||
|
|
||||||
|
img, _ := gtk.ImageNewFromIconName(iconName, gtk.ICON_SIZE_DND)
|
||||||
|
hBox.PackStart(img, false, false, 0)
|
||||||
|
|
||||||
|
if len(displayName) > 45 {
|
||||||
|
displayName = fmt.Sprintf("%s...", displayName[:42])
|
||||||
|
}
|
||||||
|
lbl, _ := gtk.LabelNew(displayName)
|
||||||
|
hBox.PackStart(lbl, false, false, 0)
|
||||||
|
row.Add(vBox)
|
||||||
|
|
||||||
|
row.Connect("activate", func() {
|
||||||
|
launch(fmt.Sprintf("%s %s", *fileManager, userDirsMap[entryName]), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
eventBox.Connect("button-release-event", func(row *gtk.ListBoxRow, e *gdk.Event) bool {
|
||||||
|
btnEvent := gdk.EventButtonNewFromEvent(e)
|
||||||
|
if btnEvent.Button() == 1 {
|
||||||
|
launch(fmt.Sprintf("%s %s", *fileManager, userDirsMap[entryName]), false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUpUserFileSearchResultRow(fileName, filePath string) *gtk.ListBoxRow {
|
||||||
|
row, _ := gtk.ListBoxRowNew()
|
||||||
|
//row.SetCanFocus(false)
|
||||||
|
row.SetSelectable(false)
|
||||||
|
vBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
eventBox, _ := gtk.EventBoxNew()
|
||||||
|
hBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
|
eventBox.Add(hBox)
|
||||||
|
vBox.PackStart(eventBox, false, false, *itemPadding)
|
||||||
|
|
||||||
|
if len(fileName) > 45 {
|
||||||
|
fileName = fmt.Sprintf("%s...", fileName[:42])
|
||||||
|
}
|
||||||
|
lbl, _ := gtk.LabelNew(fileName)
|
||||||
|
hBox.PackStart(lbl, false, false, 0)
|
||||||
|
row.Add(vBox)
|
||||||
|
|
||||||
|
row.Connect("activate", func() {
|
||||||
|
open(filePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
eventBox.Connect("button-release-event", func(row *gtk.ListBoxRow, e *gdk.Event) bool {
|
||||||
|
btnEvent := gdk.EventButtonNewFromEvent(e)
|
||||||
|
if btnEvent.Button() == 1 {
|
||||||
|
open(filePath)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUpButtonBox() *gtk.EventBox {
|
||||||
|
eventBox, _ := gtk.EventBoxNew()
|
||||||
|
wrapperHbox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
|
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
|
wrapperHbox.PackStart(box, true, true, 10)
|
||||||
|
eventBox.Add(wrapperHbox)
|
||||||
|
|
||||||
|
btn, _ := gtk.ButtonNew()
|
||||||
|
pixbuf, _ := createPixbuf("system-lock-screen", *iconSizeLarge)
|
||||||
|
img, _ := gtk.ImageNewFromPixbuf(pixbuf)
|
||||||
|
btn.SetImage(img)
|
||||||
|
btn.SetCanFocus(false)
|
||||||
|
box.PackStart(btn, true, true, 6)
|
||||||
|
btn.Connect("clicked", func() {
|
||||||
|
launch(*cmdLock, false)
|
||||||
|
//confirmationBox = setUpConfirmationBox("system-lock-screen", *cmdLock)
|
||||||
|
buttonBox.Hide()
|
||||||
|
})
|
||||||
|
|
||||||
|
btn, _ = gtk.ButtonNew()
|
||||||
|
pixbuf, _ = createPixbuf("system-log-out", *iconSizeLarge)
|
||||||
|
img, _ = gtk.ImageNewFromPixbuf(pixbuf)
|
||||||
|
btn.SetImage(img)
|
||||||
|
btn.SetCanFocus(false)
|
||||||
|
box.PackStart(btn, true, true, 6)
|
||||||
|
btn.Connect("clicked", func() {
|
||||||
|
confirmationBox = setUpConfirmationBox("system-log-out", *cmdLogout)
|
||||||
|
buttonBox.Hide()
|
||||||
|
})
|
||||||
|
|
||||||
|
btn, _ = gtk.ButtonNew()
|
||||||
|
pixbuf, _ = createPixbuf("system-reboot", *iconSizeLarge)
|
||||||
|
img, _ = gtk.ImageNewFromPixbuf(pixbuf)
|
||||||
|
btn.SetImage(img)
|
||||||
|
btn.SetCanFocus(false)
|
||||||
|
box.PackStart(btn, true, true, 6)
|
||||||
|
btn.Connect("clicked", func() {
|
||||||
|
confirmationBox = setUpConfirmationBox("system-reboot", *cmdRestart)
|
||||||
|
buttonBox.Hide()
|
||||||
|
})
|
||||||
|
|
||||||
|
btn, _ = gtk.ButtonNew()
|
||||||
|
pixbuf, _ = createPixbuf("system-shutdown", *iconSizeLarge)
|
||||||
|
img, _ = gtk.ImageNewFromPixbuf(pixbuf)
|
||||||
|
btn.SetImage(img)
|
||||||
|
btn.SetCanFocus(false)
|
||||||
|
box.PackStart(btn, true, true, 6)
|
||||||
|
btn.Connect("clicked", func() {
|
||||||
|
confirmationBox = setUpConfirmationBox("system-shutdown", *cmdShutdown)
|
||||||
|
buttonBox.Hide()
|
||||||
|
})
|
||||||
|
|
||||||
|
eventBox.Connect("enter-notify-event", func() {
|
||||||
|
cancelClose()
|
||||||
|
})
|
||||||
|
|
||||||
|
return eventBox
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUpConfirmationBox(icon string, command string) *gtk.Box {
|
||||||
|
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
|
|
||||||
|
btn, _ := gtk.ButtonNew()
|
||||||
|
pixbuf, _ := createPixbuf(icon, *iconSizeLarge)
|
||||||
|
img, _ := gtk.ImageNewFromPixbuf(pixbuf)
|
||||||
|
btn.SetImage(img)
|
||||||
|
btn.SetCanFocus(false)
|
||||||
|
box.PackEnd(btn, false, false, 6)
|
||||||
|
btn.Connect("clicked", func() {
|
||||||
|
defer restoreButtonBox()
|
||||||
|
launch(command, false)
|
||||||
|
|
||||||
|
})
|
||||||
|
btn.Connect("enter-notify-event", func() {
|
||||||
|
cancelClose()
|
||||||
|
})
|
||||||
|
|
||||||
|
btn, _ = gtk.ButtonNew()
|
||||||
|
pixbuf, _ = createPixbuf("dialog-cancel", *iconSizeLarge)
|
||||||
|
img, _ = gtk.ImageNewFromPixbuf(pixbuf)
|
||||||
|
btn.SetImage(img)
|
||||||
|
btn.SetCanFocus(false)
|
||||||
|
box.PackEnd(btn, false, false, 6)
|
||||||
|
btn.Connect("clicked", func() {
|
||||||
|
restoreButtonBox()
|
||||||
|
})
|
||||||
|
btn.Connect("enter-notify-event", func() {
|
||||||
|
cancelClose()
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonsWrapper.PackEnd(box, false, false, 10)
|
||||||
|
|
||||||
|
box.ShowAll()
|
||||||
|
w := buttonBox.GetAllocatedWidth()
|
||||||
|
h := buttonBox.GetAllocatedHeight()
|
||||||
|
box.SetSizeRequest(w, h)
|
||||||
|
box.SetHExpand(false)
|
||||||
|
|
||||||
|
return box
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreButtonBox() {
|
||||||
|
if confirmationBox != nil {
|
||||||
|
confirmationBox.Destroy()
|
||||||
|
}
|
||||||
|
if !buttonBox.IsVisible() {
|
||||||
|
buttonBox.Show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearSearchResult() {
|
||||||
|
if resultWindow != nil {
|
||||||
|
resultWindow.Destroy()
|
||||||
|
}
|
||||||
|
if fileSearchResultWindow != nil {
|
||||||
|
fileSearchResultWindow.Destroy()
|
||||||
|
}
|
||||||
|
if userDirsListBox != nil {
|
||||||
|
userDirsListBox.ShowAll()
|
||||||
|
}
|
||||||
|
if categoriesListBox != nil {
|
||||||
|
sr := categoriesListBox.GetSelectedRow()
|
||||||
|
if sr != nil {
|
||||||
|
categoriesListBox.GetSelectedRow().SetSelectable(false)
|
||||||
|
}
|
||||||
|
categoriesListBox.UnselectAll()
|
||||||
|
}
|
||||||
|
backButton.Hide()
|
||||||
|
//searchEntry.SetText("")
|
||||||
|
//searchEntry.GrabFocus()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user