58 Commits

Author SHA1 Message Date
Piotr Miller
7b76c271d9 Merge pull request #25 from nwg-piotr/logging
Logging + 1 bug fixed
2021-09-17 02:52:44 +02:00
piotr
3768e21b61 bump to 0.1.12 2021-09-17 02:38:09 +02:00
piotr
c1a807615f avoid closing window on item pinned 2021-09-17 02:35:43 +02:00
piotr
b6dd4099fd logging with logrus 2021-09-17 02:33:39 +02:00
Piotr Miller
6ca8e6a175 Merge pull request #24 from nwg-piotr/fix_nodisplay
Fix nodisplay, fix running in terminal
2021-09-07 02:51:26 +02:00
piotr
f12be46583 fix #23 2021-09-07 02:47:17 +02:00
piotr
9d50258042 don't skip hidden entries: fixes #22 added in #19 2021-09-07 02:07:29 +02:00
piotr
1be8e432fa print errors in human-readable format 2021-09-05 01:57:24 +02:00
piotr
aa227b51b8 update README 2021-09-05 01:35:44 +02:00
piotr
56d6cd264d update README 2021-09-05 01:34:57 +02:00
piotr
bea2fdf90c verrsion bump 2021-09-05 01:26:36 +02:00
Piotr Miller
6a0f9d4a93 Merge pull request #20 from james-lawrence/use-terminal-env
use terminal environment variable if present
2021-09-05 01:12:30 +02:00
James Lawrence
206a602e77 use terminal environment variable if present 2021-09-04 15:29:58 -04:00
Piotr Miller
c655f99cd9 Merge pull request #19 from james-lawrence/bugfix/whitespace-handling
Bugfix/whitespace handling
2021-09-01 02:18:26 +02:00
James Lawrence
3a59228eaa remove old parse entry version 2021-08-31 06:33:04 -04:00
James Lawrence
57826c064d improve parsing of desktop entries.
- speed up parsing by not creating nearly as many strings.
- fix basic whitespace handling. fixes #4

goos: linux
goarch: amd64
pkg: github.com/nwg-piotr/nwg-drawer
cpu: AMD Ryzen 7 1800X Eight-Core Processor
BenchmarkDesktopEntryParserOld-16    	   10000	    146094 ns/op
BenchmarkDesktopEntryParser-16       	   26097	     43303 ns/op
PASS
ok  	github.com/nwg-piotr/nwg-drawer	3.090s
2021-08-31 06:32:40 -04:00
Piotr Miller
c5e39f179f Merge pull request #18 from nwg-piotr/searchres
focus first search result #17
2021-08-30 03:17:49 +02:00
piotr
ce48848589 focus first search result #17 2021-08-29 03:37:00 +02:00
Piotr Miller
9d0c4d2e5f Merge pull request #16 from nwg-piotr/fixnofs
fix crash on a category button click while file search turned off (`-nofs`)
2021-08-27 01:33:01 +02:00
piotr
a7968d8510 version bump 2021-08-27 01:23:16 +02:00
piotr
e601316480 add missing nil check #15 2021-08-27 01:21:41 +02:00
piotr
4818dc3358 update README 2021-08-21 01:45:22 +02:00
piotr
9afd7dca20 update README 2021-08-21 01:41:45 +02:00
piotr
eb1e9994e7 update README 2021-08-21 01:40:19 +02:00
Piotr Miller
3e2b9395d5 Merge pull request #14 from nwg-piotr/files
xdg-open support + customization + 1 bug fixed
2021-08-21 00:49:24 +02:00
piotr
061d5e1ca9 avoid unwanted closes 2021-08-21 00:34:50 +02:00
piotr
dc50203ee3 update README 2021-08-20 05:43:16 +02:00
piotr
656347855b update README 2021-08-20 05:40:51 +02:00
piotr
81aeb10311 update README 2021-08-20 05:36:27 +02:00
piotr
04a292c2ec check err just in case 2021-08-20 05:02:37 +02:00
piotr
6912e7f320 xdg-open or open in FM 2021-08-20 04:46:26 +02:00
piotr
41d48a0824 version bump 2021-08-17 22:49:09 +02:00
Piotr Miller
399cf7fa3a Merge pull request #13 from nwg-piotr/files
hot fix to opening files with file manager
2021-08-17 22:35:43 +02:00
piotr
aefe2af4d5 hot fix to opening files 2021-08-17 22:34:28 +02:00
piotr
71892c25ed update README 2021-08-17 13:11:07 +02:00
piotr
2536331184 update README 2021-08-17 13:06:24 +02:00
Piotr Miller
6ac486a32c Merge pull request #12 from nwg-piotr/chrome
bug fixes and new arguments
2021-08-17 01:25:13 +02:00
piotr
875e7cac4a Quit immediately on window click 2021-08-17 00:54:52 +02:00
piotr
2960603462 -nofs to disable file search 2021-08-16 23:26:30 +02:00
piotr
f9e9608dfc -nocats to disable filtering by categories 2021-08-16 22:41:13 +02:00
piotr
83caef28f6 fresh binary 2021-08-15 14:37:06 +02:00
piotr
d732e9e070 support env vars at the end of the command #11 2021-08-15 14:34:25 +02:00
piotr
033a4d59ae refresh binary 2021-07-21 02:07:35 +02:00
Piotr Miller
97be33094f Merge pull request #9 from nwg-piotr/force_img
SetAlwaysShowImage(true) in category button
2021-07-21 01:55:17 +02:00
piotr
e5246a13cd force image in category button #8 2021-07-21 01:52:19 +02:00
piotr
897d693d62 force image in category button 2021-07-21 01:49:34 +02:00
Piotr Miller
955429d809 Merge pull request #7 from nwg-piotr/fix_pinned
Fix GTK critical warnings on pinned cache file not found
2021-07-12 23:32:41 +02:00
piotr
4bf82d9a98 bump to 0.1.4 2021-07-12 23:26:26 +02:00
piotr
eb358b24cd fix GTK warnings on pinned cache not found #6 2021-07-12 23:23:52 +02:00
Piotr Miller
27718a9f1b Merge pull request #5 from nwg-piotr/nodisplay
More appearance personalization
2021-06-29 23:54:17 +02:00
piotr
b4f0f70f56 default css modified 2021-06-29 17:32:44 +02:00
piotr
f54be1c13a name property for category buttons 2021-06-29 17:24:48 +02:00
piotr
cd6e544adc wrong resources path fixed 2021-06-26 23:45:46 +02:00
piotr
f2bda0e496 bump to 0.1.1 2021-06-23 02:55:02 +02:00
piotr
fd8b9ba128 uninstall fixed 2021-06-23 02:53:56 +02:00
Piotr Miller
94aba90f3d Merge pull request #3 from nwg-piotr/sammiev
basic X11 support
2021-06-23 02:48:14 +02:00
piotr
fa9d9c3425 basic X11 support 2021-06-23 02:44:56 +02:00
Piotr Miller
4711c36c56 Update README.md 2021-06-22 12:00:15 +02:00
11 changed files with 470 additions and 194 deletions

View File

@@ -5,6 +5,7 @@ get:
go get github.com/dlasky/gotk3-layershell/layershell
go get github.com/joshuarubin/go-sway
go get github.com/allan-simon/go-singleinstance
go get "github.com/sirupsen/logrus"
build:
go build -o bin/nwg-drawer *.go
@@ -16,7 +17,9 @@ install:
cp bin/nwg-drawer /usr/bin
uninstall:
rm -r /usr/share/nwg-drawer
rm /usr/bin/nwg-drawer
run:
go run *.go
go build -o bin/nwg-drawer *.go
bin/nwg-drawer

View File

@@ -5,7 +5,8 @@ This application is a part of the [nwg-shell](https://github.com/nwg-piotr/nwg-s
Nwg-drawer is a golang replacement to the `nwggrid` command
(a part of [nwg-launchers](https://github.com/nwg-piotr/nwg-launchers)). It's being developed with
[sway](https://github.com/swaywm/sway) in mind, but should also work with other wlroots-based Wayland compositors.
X Window System is not supported.
X Window System is not officially supported, but you should be able to use the drawer on some floating
window managers (tested on Openbox).
The `nwg-drawer` command displays the application grid. The search entry allows to look for installed applications,
and for files in XDG user directories. The grid view may also be filtered by categories.
@@ -18,21 +19,24 @@ and `nwggrid`.
[more screenshots](https://scrot.cloud/album/nwg-drawer.Bogd) | [see on YouTube](https://youtu.be/iIgxJQhCQf0)
[![Packaging status](https://repology.org/badge/vertical-allrepos/nwg-drawer.svg)](https://repology.org/project/nwg-drawer/versions)
## Installation
### Dependencies
- go 1.16 (just to build)
- go >=1.16 (just to build)
- gtk3
- gtk-layer-shell
- xdg-utils
Optional (recommended):
- thunar
- alacritty
You may use another file manager and terminal emulator (see command line arguments), but for now the program has
only been tested with the two mentioned above.
You may use another file manager and terminal emulator (see command line arguments), but mentioned above have been
confirmed to work well with the program. Also see **Files** below.
### Steps
@@ -61,8 +65,12 @@ Usage of nwg-drawer:
Icon Size (default 64)
-lang string
force lang, e.g. "en", "pl"
-nocats
Disable filtering by category
-nofs
Disable file search
-o string
name of the Output to display the menu on (sway only)
name of the Output to display the drawer on (sway only)
-ovl
use OVerLay layer
-s string
@@ -74,10 +82,43 @@ Usage of nwg-drawer:
-v display Version information
```
*NOTE: the `$TERM` environment variable overrides the `-term` argument if defined.*
## Styling
Edit `~/.config/nwg-panel/drawer.css` to your taste.
## Files
When the search phrase is at least 3 characters long, your XDG user directories are being searched.
![screenshot-03.png](https://scrot.cloud/images/2021/05/30/screenshot-03.png)
Use the **left mouse button** to open a file with the `xdg-open` command. As configuring file associations for it is
PITA, you may override them, by creating the `~/.config/nwg-panel/preferred-apps.json` file with your own definitions.
### Sample file content
```json
{
"\\.pdf$": "atril",
"\\.svg$": "inkscape",
"\\.(jpg|png|tiff|gif)$": "feh",
"\\.(mp3|ogg|flac|wav|wma)$": "audacious",
"\\.(avi|mp4|mkv|mov|wav)$": "mpv",
"\\.(doc|docx|xls|xlsx)$": "libreoffice"
}
```
Use the **right mouse button** to open the file with your file manager (see `-fm` argument). The result depends on the
file manager you use.
- thunar will open the file location
- pcmanfm will open the file with its associated program
- caja won't open anything, except for directories
I've noy yet tried other file managers.
## Credits
This program uses some great libraries:

Binary file not shown.

View File

@@ -3,11 +3,12 @@ window {
color: #eeeeee
}
/* search entry */
entry {
background-color: rgba (0, 0, 0, 0.2)
}
button {
button, image {
background: none;
border: none
}
@@ -16,6 +17,11 @@ button:hover {
background-color: rgba (255, 255, 255, 0.1)
}
/* in case you wanted to give category buttons a different look */
#category-button {
margin: 0 10px 0 10px
}
#pinned-box {
padding-bottom: 5px;
border-bottom: 1px dotted gray

3
go.mod
View File

@@ -1,4 +1,4 @@
module github.com/nwg-piotr/nwg-menu
module github.com/nwg-piotr/nwg-drawer
go 1.16
@@ -7,4 +7,5 @@ require (
github.com/dlasky/gotk3-layershell v0.0.0-20210331230524-5cca0b819261
github.com/gotk3/gotk3 v0.6.0
github.com/joshuarubin/go-sway v0.0.4
github.com/sirupsen/logrus v1.8.1
)

8
go.sum
View File

@@ -1,7 +1,8 @@
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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/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/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
@@ -13,7 +14,10 @@ github.com/joshuarubin/lifecycle v1.0.0 h1:N/lPEC8f+dBZ1Tn99vShqp36LwB+LI7XNAiNa
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/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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=
@@ -22,3 +26,5 @@ 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=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

90
main.go
View File

@@ -3,7 +3,6 @@ package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
@@ -12,6 +11,8 @@ import (
"syscall"
"time"
log "github.com/sirupsen/logrus"
"github.com/allan-simon/go-singleinstance"
"github.com/dlasky/gotk3-layershell/layershell"
"github.com/gotk3/gotk3/gdk"
@@ -19,7 +20,7 @@ import (
"github.com/gotk3/gotk3/gtk"
)
const version = "0.1.0"
const version = "0.1.12"
var (
appDirs []string
@@ -28,6 +29,7 @@ var (
pinned []string
src glib.SourceHandle
id2entry map[string]desktopEntry
preferredApps map[string]interface{}
)
var categoryNames = [...]string{
@@ -58,6 +60,7 @@ type desktopEntry struct {
CommentLoc string
Icon string
Exec string
Category string
Terminal bool
NoDisplay bool
}
@@ -90,15 +93,26 @@ var (
fileSearchResultWrapper *gtk.Box
pinnedFlowBox *gtk.FlowBox
pinnedFlowBoxWrapper *gtk.Box
categoriesWrapper *gtk.Box
catButtons []*gtk.Button
statusLabel *gtk.Label
status string
ignore string
)
func defaultStringIfBlank(s, fallback string) string {
s = strings.TrimSpace(s)
// os.Getenv("TERM") returns "linux" instead of empty string, if program has been started
// from a key binding defined in the config file. See #23.
if s == "" || s == "linux" {
return fallback
}
return s
}
// Flags
var cssFileName = flag.String("s", "drawer.css", "Styling: css file name")
var targetOutput = flag.String("o", "", "name of the Output to display the menu on (sway only)")
var targetOutput = flag.String("o", "", "name of the Output to display the drawer on (sway only)")
var displayVersion = flag.Bool("v", false, "display Version information")
var overlay = flag.Bool("ovl", false, "use OVerLay layer")
var iconSize = flag.Int("is", 64, "Icon Size")
@@ -107,8 +121,10 @@ var columnsNumber = flag.Uint("c", 6, "number of Columns")
var itemSpacing = flag.Uint("spacing", 20, "icon spacing")
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 term = flag.String("term", defaultStringIfBlank(os.Getenv("TERM"), "alacritty"), "Terminal emulator")
var nameLimit = flag.Int("fslen", 80, "File Search name length Limit")
var noCats = flag.Bool("nocats", false, "Disable filtering by category")
var noFS = flag.Bool("nofs", false, "Disable file search")
func main() {
timeStart := time.Now()
@@ -126,7 +142,7 @@ func main() {
for {
s := <-signalChan
if s == syscall.SIGTERM {
println("SIGTERM received, bye bye!")
log.Info("SIGTERM received, bye bye!")
gtk.MainQuit()
}
}
@@ -140,7 +156,7 @@ func main() {
if err == nil {
i, err := strconv.Atoi(pid)
if err == nil {
println("Running instance found, sending SIGTERM and exiting...")
log.Info("Running instance found, sending SIGTERM and exiting...")
syscall.Kill(i, syscall.SIGTERM)
}
}
@@ -152,7 +168,7 @@ func main() {
if *lang == "" && os.Getenv("LANG") != "" {
*lang = strings.Split(os.Getenv("LANG"), ".")[0]
}
println(fmt.Sprintf("lang: %s", *lang))
log.Info(fmt.Sprintf("lang: %s", *lang))
// ENVIRONMENT
configDirectory = configDir()
@@ -172,6 +188,7 @@ func main() {
if err != nil {
pinned = nil
}
log.Info(fmt.Sprintf("Found %v pinned items", len(pinned)))
cssFile := filepath.Join(configDirectory, *cssFileName)
@@ -180,10 +197,20 @@ func main() {
setUpCategories()
desktopFiles := listDesktopFiles()
println(fmt.Sprintf("Found %v desktop files", len(desktopFiles)))
log.Info(fmt.Sprintf("Found %v desktop files", len(desktopFiles)))
status = parseDesktopFiles(desktopFiles)
// For opening files we use xdg-open. As its configuration is PITA, we may override some associations
// in the ~/.config/nwg-panel/preferred-apps.json file.
paFile := filepath.Join(configDirectory, "preferred-apps.json")
preferredApps, err = loadPreferredApps(paFile)
if err != nil {
log.Error(fmt.Sprintf("Custom associations file %s not found or invalid", paFile))
} else {
log.Info(fmt.Sprintf("Found %v associations in %s", len(preferredApps), paFile))
}
// USER INTERFACE
gtk.Init(nil)
@@ -191,10 +218,10 @@ func main() {
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))
log.Errorf("ERROR: %s css file not found or erroneous. Using GTK styling.", cssFile)
log.Errorf("%s", err)
} else {
println(fmt.Sprintf("Using style from %s", cssFile))
log.Info(fmt.Sprintf("Using style from %s", cssFile))
screen, _ := gdk.ScreenGetDefault()
gtk.AddProviderForScreen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
}
@@ -204,6 +231,7 @@ func main() {
log.Fatal("Unable to create window:", err)
}
if wayland() {
layershell.InitForWindow(win)
var output2mon map[string]*gdk.Monitor
@@ -215,7 +243,7 @@ func main() {
layershell.SetMonitor(win, monitor)
} else {
println(err)
log.Errorf("%s", err)
}
}
@@ -232,6 +260,7 @@ func main() {
}
layershell.SetKeyboardMode(win, layershell.LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE)
}
win.Connect("destroy", func() {
gtk.MainQuit()
@@ -251,12 +280,11 @@ func main() {
return false
case gdk.KEY_downarrow, gdk.KEY_Up, gdk.KEY_Down, gdk.KEY_Left, gdk.KEY_Right, gdk.KEY_Tab,
gdk.KEY_Return, gdk.KEY_Page_Up, gdk.KEY_Page_Down, gdk.KEY_Home, gdk.KEY_End:
//searchEntry.SetText("")
return false
default:
if !searchEntry.IsFocus() {
searchEntry.GrabFocus()
searchEntry.GrabFocusWithoutSelecting()
}
return false
}
@@ -274,6 +302,17 @@ func main() {
cancelClose()
})
/*
In case someone REALLY needed to use X11 - for some stupid Zoom meeting or something, this allows
the drawer to behave properly on Openbox, and possibly somewhere else. For sure not on i3.
This feature is not really supported and will stay undocumented.
*/
if !wayland() {
log.Info("Not Wayland, oh really?")
win.SetDecorated(false)
win.Maximize()
}
// Set up UI
outerVBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
win.Add(outerVBox)
@@ -285,10 +324,12 @@ func main() {
searchEntry.SetMaxWidthChars(30)
searchBoxWrapper.PackStart(searchEntry, true, false, 0)
categoriesWrapper, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
if !*noCats {
categoriesWrapper, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
categoriesButtonBox := setUpCategoriesButtonBox()
categoriesWrapper.PackStart(categoriesButtonBox, true, false, 0)
outerVBox.PackStart(categoriesWrapper, false, false, 0)
}
pinnedWrapper, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
outerVBox.PackStart(pinnedWrapper, false, false, 0)
@@ -298,10 +339,19 @@ func main() {
pinnedFlowBox = setUpPinnedFlowBox()
resultWindow, _ = gtk.ScrolledWindowNew(nil, nil)
resultWindow.SetEvents(int(gdk.ALL_EVENTS_MASK))
resultWindow.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
resultWindow.Connect("enter-notify-event", func() {
cancelClose()
})
resultWindow.Connect("button-release-event", func(sw *gtk.ScrolledWindow, e *gdk.Event) bool {
btnEvent := gdk.EventButtonNewFromEvent(e)
if btnEvent.Button() == 1 || btnEvent.Button() == 3 {
gtk.MainQuit()
return true
}
return false
})
outerVBox.PackStart(resultWindow, true, true, 10)
resultsWrapper, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
@@ -328,11 +378,13 @@ func main() {
resultsWrapper.PackStart(placeholder, true, true, 0)
placeholder.SetSizeRequest(20, 20)
if !*noFS {
wrapper, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
fileSearchResultWrapper, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
fileSearchResultWrapper.SetProperty("name", "files-box")
wrapper.PackStart(fileSearchResultWrapper, true, false, 0)
resultsWrapper.PackEnd(wrapper, false, false, 10)
}
statusLineWrapper, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
outerVBox.PackStart(statusLineWrapper, false, false, 10)
@@ -340,11 +392,15 @@ func main() {
statusLineWrapper.PackStart(statusLabel, true, false, 0)
win.ShowAll()
if !*noFS {
fileSearchResultWrapper.SetSizeRequest(appFlowBox.GetAllocatedWidth(), 1)
categoriesWrapper.SetSizeRequest(1, categoriesWrapper.GetAllocatedHeight()*2)
fileSearchResultWrapper.Hide()
}
if !*noCats {
categoriesWrapper.SetSizeRequest(1, categoriesWrapper.GetAllocatedHeight()*2)
}
t := time.Now()
println(fmt.Sprintf("UI created in %v ms. Thank you for your patience.", t.Sub(timeStart).Milliseconds()))
log.Info(fmt.Sprintf("UI created in %v ms. Thank you for your patience.", t.Sub(timeStart).Milliseconds()))
gtk.Main()
}

188
tools.go
View File

@@ -2,24 +2,32 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
"github.com/joshuarubin/go-sway"
)
func wayland() bool {
return os.Getenv("WAYLAND_DISPLAY") != "" || os.Getenv("XDG_SESSION_TYPE") == "wayland"
}
/*
Window leave-notify-event event quits the program with glib Timeout 500 ms.
We might have left the window by accident, so let's clear the timeout if window re-entered.
@@ -42,7 +50,7 @@ func createPixbuf(icon string, size int) (*gdk.Pixbuf, error) {
if strings.Contains(icon, "/") {
pixbuf, err := gdk.PixbufNewFromFileAtSize(icon, size, size)
if err != nil {
println(err)
log.Errorf("%s", err)
return nil, err
}
return pixbuf, nil
@@ -85,7 +93,7 @@ func mapXdgUserDirs() map[string]string {
userDirsFile := filepath.Join(home, ".config/user-dirs.dirs")
if pathExists(userDirsFile) {
println(fmt.Sprintf("Using XDG user dirs from %s", userDirsFile))
log.Info(fmt.Sprintf("Using XDG user dirs from %s", userDirsFile))
lines, _ := loadTextFile(userDirsFile)
for _, l := range lines {
if strings.HasPrefix(l, "XDG_DOCUMENTS_DIR") {
@@ -109,7 +117,7 @@ func mapXdgUserDirs() map[string]string {
}
}
} else {
println(fmt.Sprintf("%s file not found, using defaults", userDirsFile))
log.Warnf("%s file not found, using defaults", userDirsFile)
}
return result
@@ -170,13 +178,13 @@ 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)
log.Infof("Creating dir: %s", dir)
}
}
}
func copyFile(src, dst string) error {
fmt.Println("Copying file:", dst)
log.Infof("Copying file: %s", dst)
var err error
var srcfd *os.File
@@ -239,6 +247,25 @@ func getAppDirs() []string {
return dirs
}
func loadPreferredApps(path string) (map[string]interface{}, error) {
jsonFile, err := os.Open(path)
if err != nil {
return nil, err
}
defer jsonFile.Close()
byteValue, _ := ioutil.ReadAll(jsonFile)
var result map[string]interface{}
json.Unmarshal([]byte(byteValue), &result)
if len(result) == 0 {
return nil, errors.New("json invalid or empty")
}
return result, nil
}
func listFiles(dir string) ([]fs.FileInfo, error) {
files, err := ioutil.ReadDir(dir)
if err == nil {
@@ -264,7 +291,7 @@ func listDesktopFiles() []string {
}
func setUpCategories() {
path := filepath.Join(getDataHome(), "nwg-menu/desktop-directories")
path := filepath.Join(getDataHome(), "nwg-drawer/desktop-directories")
var other category
for _, cName := range categoryNames {
@@ -324,109 +351,35 @@ func setUpCategories() {
func parseDesktopFiles(desktopFiles []string) 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
id := filepath.Base(file)
if _, ok := id2entry[id]; ok {
skipped++
continue
}
categories := ""
entry, err := parseDesktopEntryFile(id, file)
if err != nil {
continue
}
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 {
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
if entry.NoDisplay {
hidden++
continue
// We still need hidden entries, so `continue` is disallowed here
// Fixes introduced in #19
}
}
// 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++
}
}
desktopEntries = append(desktopEntries, entry)
assignToLists(entry.DesktopID, entry.Category)
}
sort.Slice(desktopEntries, func(i, j int) bool {
return desktopEntries[i].NameLoc < desktopEntries[j].NameLoc
})
summary := fmt.Sprintf("%v entries (+%v hidden)", len(desktopEntries)-hidden, hidden)
println(fmt.Sprintf("Skipped %v duplicates; %v .desktop entries hidden by \"NoDisplay=true\"", skipped, hidden))
log.Infof("Skipped %v duplicates; %v .desktop entries hidden by \"NoDisplay=true\"", skipped, hidden)
return summary
}
@@ -519,20 +472,20 @@ func loadTextFile(path string) ([]string, error) {
func pinItem(itemID string) {
for _, item := range pinned {
if item == itemID {
println(item, "already pinned")
log.Warn(item, "already pinned")
return
}
}
pinned = append(pinned, itemID)
savePinned()
println(itemID, "pinned")
log.Infof("%s pinned", itemID)
}
func unpinItem(itemID string) {
if isIn(pinned, itemID) {
pinned = remove(pinned, itemID)
savePinned()
println(itemID, "unpinned")
log.Infof("%s unpinned", itemID)
}
}
@@ -558,7 +511,7 @@ func savePinned() {
_, err := f.WriteString(line + "\n")
if err != nil {
println("Error saving pinned", err)
log.Errorf("Error saving pinned", err)
}
}
}
@@ -579,17 +532,19 @@ func launch(command string, terminal bool) {
envVarsNum := strings.Count(command, "=")
var envVars []string
cmdIdx := 0
lastEnvVarIdx := 0
cmdIdx := -1
if envVarsNum > 0 {
for idx, item := range elements {
if strings.Contains(item, "=") {
lastEnvVarIdx = idx
envVars = append(envVars, item)
} else if !strings.HasPrefix(item, "-") && cmdIdx == -1 {
cmdIdx = idx
}
}
cmdIdx = lastEnvVarIdx + 1
}
if cmdIdx == -1 {
cmdIdx = 0
}
cmd := exec.Command(elements[cmdIdx], elements[1+cmdIdx:]...)
@@ -606,7 +561,7 @@ func launch(command string, terminal bool) {
}
msg := fmt.Sprintf("env vars: %s; command: '%s'; args: %s\n", envVars, elements[cmdIdx], elements[1+cmdIdx:])
println(msg)
log.Info(msg)
go cmd.Run()
@@ -616,14 +571,25 @@ func launch(command string, terminal bool) {
})
}
func open(filePath string) {
cmd := exec.Command(*fileManager, filePath)
go cmd.Run()
func open(filePath string, xdgOpen bool) {
var cmd *exec.Cmd
if xdgOpen {
cmd = exec.Command("xdg-open", filePath)
// Look for possible custom file association
for key, element := range preferredApps {
r, err := regexp.Compile(key)
if err == nil && r.MatchString(filePath) {
cmd = exec.Command(fmt.Sprintf("%v", element), filePath)
break
}
}
} else {
cmd = exec.Command(*fileManager, filePath)
}
fmt.Printf("Executing: %s", cmd)
cmd.Start()
glib.TimeoutAdd(uint(150), func() bool {
gtk.MainQuit()
return false
})
}
// Returns map output name -> gdk.Monitor

View File

@@ -17,7 +17,7 @@ func setUpPinnedFlowBox() *gtk.FlowBox {
flowBox, _ := gtk.FlowBoxNew()
if uint(len(pinned)) >= *columnsNumber {
flowBox.SetMaxChildrenPerLine(*columnsNumber)
} else {
} else if len(pinned) > 0 {
flowBox.SetMaxChildrenPerLine(uint(len(pinned)))
}
@@ -78,16 +78,17 @@ func setUpPinnedFlowBox() *gtk.FlowBox {
})
flowBox.Add(btn)
}
pinnedFlowBoxWrapper.PackStart(flowBox, true, false, 0)
//While moving focus with arrow keys we want buttons to get focus directly
flowBox.GetChildren().Foreach(func(item interface{}) {
item.(*gtk.Widget).SetCanFocus(false)
})
}
flowBox.Connect("enter-notify-event", func() {
cancelClose()
})
pinnedFlowBoxWrapper.PackStart(flowBox, true, false, 0)
//While moving focus with arrow keys we want buttons to get focus directly
flowBox.GetChildren().Foreach(func(item interface{}) {
item.(*gtk.Widget).SetCanFocus(false)
})
flowBox.ShowAll()
return flowBox
@@ -113,6 +114,7 @@ func setUpCategoriesButtonBox() *gtk.EventBox {
hBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
eventBox.Add(hBox)
button, _ := gtk.ButtonNewWithLabel("All")
button.SetProperty("name", "category-button")
button.Connect("clicked", func(item *gtk.Button) {
searchEntry.SetText("")
appFlowBox = setUpAppsFlowBox(nil, "")
@@ -126,8 +128,11 @@ func setUpCategoriesButtonBox() *gtk.EventBox {
for _, cat := range categories {
if isSupposedToShowUp(cat.Name) {
button, _ = gtk.ButtonNewFromIconName(cat.Icon, gtk.ICON_SIZE_MENU)
button.SetProperty("name", "category-button")
catButtons = append(catButtons, button)
button.SetLabel(cat.DisplayName)
// fix #8
button.SetAlwaysShowImage(true)
hBox.PackStart(button, false, false, 0)
name := cat.Name
b := *button
@@ -141,7 +146,9 @@ func setUpCategoriesButtonBox() *gtk.EventBox {
w := b.GetAllocatedWidth()
b.SetImagePosition(gtk.POS_TOP)
b.SetSizeRequest(w, 0)
if fileSearchResultWrapper != nil {
fileSearchResultWrapper.Hide()
}
})
}
}
@@ -256,6 +263,7 @@ func flowBoxButton(entry desktopEntry) *gtk.Button {
} else if btnEvent.Button() == 3 {
pinItem(ID)
pinnedFlowBox = setUpPinnedFlowBox()
return true
}
return false
})
@@ -314,13 +322,17 @@ func setUpSearchEntry() *gtk.SearchEntry {
phrase, _ = searchEntry.GetText()
if len(phrase) > 0 {
// search apps
appFlowBox = setUpAppsFlowBox(nil, phrase)
if len(phrase) > 2 {
// search files
if !*noFS && len(phrase) > 2 {
if fileSearchResultFlowBox != nil {
fileSearchResultFlowBox.Destroy()
}
fileSearchResultFlowBox = setUpFileSearchResultContainer()
for key := range userDirsMap {
if key != "home" {
fileSearchResults = nil
@@ -332,22 +344,53 @@ func setUpSearchEntry() *gtk.SearchEntry {
statusLabel.SetText("0 results")
}
} else {
// search phrase too short
if fileSearchResultFlowBox != nil {
fileSearchResultFlowBox.Destroy()
}
if fileSearchResultWrapper != nil {
fileSearchResultWrapper.Hide()
}
}
// focus 1st search result #17
var w *gtk.Widget
if appFlowBox != nil {
b := appFlowBox.GetChildAtIndex(0)
if b != nil {
button, err := b.GetChild()
if err == nil {
button.ToWidget().GrabFocus()
w = button.ToWidget()
}
}
}
if w == nil && fileSearchResultFlowBox != nil {
f := fileSearchResultFlowBox.GetChildAtIndex(0)
if f != nil {
//f.SetCanFocus(false)
button, err := f.GetChild()
if err == nil {
button.ToWidget().SetCanFocus(true)
button.ToWidget().GrabFocus()
}
}
}
} else {
// clear search results
appFlowBox = setUpAppsFlowBox(nil, "")
if fileSearchResultFlowBox != nil {
fileSearchResultFlowBox.Destroy()
}
appFlowBox = setUpAppsFlowBox(nil, "")
if fileSearchResultWrapper != nil {
fileSearchResultWrapper.Hide()
}
}
})
searchEntry.Connect("focus-in-event", func() {
/*searchEntry.Connect("focus-in-event", func() {
searchEntry.SetText("")
})
})*/
return searchEntry
}
@@ -370,7 +413,8 @@ func searchUserDir(dir string) {
}
fileSearchResultFlowBox.Hide()
statusLabel.SetText(fmt.Sprintf("%v results", fileSearchResultFlowBox.GetChildren().Length()))
statusLabel.SetText(fmt.Sprintf("%v results | LMB: xdg-open | RMB: file manager",
fileSearchResultFlowBox.GetChildren().Length()))
num := uint(fileSearchResultFlowBox.GetChildren().Length() / *fsColumns)
fileSearchResultFlowBox.SetMinChildrenPerLine(num + 1)
fileSearchResultFlowBox.SetMaxChildrenPerLine(num + 1)
@@ -389,6 +433,7 @@ func setUpUserDirButton(iconName, displayName, entryName string, userDirsMap map
}
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
button, _ := gtk.ButtonNew()
button.SetAlwaysShowImage(true)
img, _ := gtk.ImageNewFromIconName(iconName, gtk.ICON_SIZE_MENU)
button.SetImage(img)
@@ -397,8 +442,16 @@ func setUpUserDirButton(iconName, displayName, entryName string, userDirsMap map
}
button.SetLabel(displayName)
button.Connect("clicked", func() {
launch(fmt.Sprintf("%s %s", *fileManager, userDirsMap[entryName]), false)
button.Connect("button-release-event", func(btn *gtk.Button, e *gdk.Event) bool {
btnEvent := gdk.EventButtonNewFromEvent(e)
if btnEvent.Button() == 1 {
open(userDirsMap[entryName], true)
return true
} else if btnEvent.Button() == 3 {
open(userDirsMap[entryName], false)
return true
}
return false
})
box.PackStart(button, false, true, 0)
@@ -413,6 +466,7 @@ func setUpUserFileSearchResultButton(fileName, filePath string) *gtk.Box {
if strings.HasPrefix(filePath, "#is_dir#") {
filePath = filePath[8:]
img, _ := gtk.ImageNewFromIconName("folder", gtk.ICON_SIZE_MENU)
button.SetAlwaysShowImage(true)
button.SetImage(img)
}
@@ -426,8 +480,20 @@ func setUpUserFileSearchResultButton(fileName, filePath string) *gtk.Box {
button.SetTooltipText(tooltipText)
}
button.Connect("clicked", func() {
open(filePath)
button.Connect("button-release-event", func(btn *gtk.Button, e *gdk.Event) bool {
btnEvent := gdk.EventButtonNewFromEvent(e)
if btnEvent.Button() == 1 {
open(filePath, true)
return true
} else if btnEvent.Button() == 3 {
open(filePath, false)
return true
}
return false
})
button.Connect("activate", func() {
open(filePath, true)
})
box.PackStart(button, false, true, 0)

78
xdgdesktop_parser.go Normal file
View File

@@ -0,0 +1,78 @@
package main
import (
"bufio"
"fmt"
"io"
"os"
"strconv"
"strings"
)
func parseDesktopEntryFile(id string, path string) (e desktopEntry, err error) {
o, err := os.Open(path)
if err != nil {
return e, err
}
defer o.Close()
return parseDesktopEntry(id, o)
}
func parseDesktopEntry(id string, in io.Reader) (entry desktopEntry, err error) {
cleanexec := strings.NewReplacer("\"", "", "'", "")
entry.DesktopID = id
localizedName := fmt.Sprintf("Name[%s]", strings.Split(*lang, "_")[0])
localizedComment := fmt.Sprintf("Comment[%s]", strings.Split(*lang, "_")[0])
scanner := bufio.NewScanner(in)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
l := scanner.Text()
if strings.HasPrefix(l, "[") && l != "[Desktop Entry]" {
break
}
name, value := parseKeypair(l)
if value == "" {
continue
}
switch name {
case "Name":
entry.Name = value
case localizedName:
entry.NameLoc = value
case "Comment":
entry.Comment = value
case localizedComment:
entry.CommentLoc = value
case "Icon":
entry.Icon = value
case "Categories":
entry.Category = value
case "Terminal":
entry.Terminal, _ = strconv.ParseBool(value)
case "NoDisplay":
entry.NoDisplay, _ = strconv.ParseBool(value)
case "Exec":
entry.Exec = cleanexec.Replace(value)
}
}
// if name[ln] not found, let's try to find name[ln_LN]
if entry.NameLoc == "" {
entry.NameLoc = entry.Name
}
if entry.CommentLoc == "" {
entry.CommentLoc = entry.Comment
}
return entry, err
}
func parseKeypair(s string) (string, string) {
if idx := strings.IndexRune(s, '='); idx > 0 {
return strings.TrimSpace(s[:idx]), strings.TrimSpace(s[idx+1:])
}
return s, ""
}

53
xdgdesktop_parser_test.go Normal file
View File

@@ -0,0 +1,53 @@
package main
import (
"strings"
"testing"
)
var result desktopEntry
func BenchmarkDesktopEntryParser(b *testing.B) {
var entry desktopEntry
for n := 0; n < b.N; n++ {
entry, _ = parseDesktopEntryFile("id", "./desktop-directories/game.directory")
}
result = entry
}
func TestWhitespaceHandling(t *testing.T) {
const whitespace = `[Desktop Entry]
Categories = Debugger; Development; Git; IDE; Programming; TextEditor;
Comment = Editor for building and debugging modern web and cloud applications
Exec = bash -c "code-insiders ~/Workspaces/Linux/Flutter.code-workspace"
GenericName = Text Editor
Icon = vscode-flutter
Keywords = editor; IDE; plaintext; text; write;
MimeType = application/x-shellscript; inode/directory; text/english; text/plain; text/x-c; text/x-c++; text/x-c++hdr; text/x-c++src; text/x-chdr; text/x-csrc; text/x-java; text/x-makefile; text/x-moc; text/x-pascal; text/x-tcl; text/x-tex;
Name = VSCode Insiders with Flutter
Name[pt] = VSCode Insiders com Flutter
StartupNotify = true
StartupWMClass = code - insiders
Terminal = false
NoDisplay = false
Type = Application
Version = 1.0`
*lang = "pt"
entry, err := parseDesktopEntry("id", strings.NewReader(whitespace))
if err != nil {
t.Fatal(err)
}
if entry.Name != "VSCode Insiders with Flutter" {
t.Error("failed to parse desktop entry name")
}
if entry.NameLoc != "VSCode Insiders com Flutter" {
t.Error("failed to parse localized name")
}
if entry.NoDisplay {
t.Error("failed to parse desktop entry no display")
}
}