Merge pull request #30 from nwg-piotr/v020

v0.2.0 allows to run a resident instance of nwg-drawer
This commit is contained in:
Piotr Miller
2021-09-29 22:49:58 +02:00
committed by GitHub
9 changed files with 381 additions and 110 deletions

View File

@@ -6,11 +6,14 @@ get:
go get github.com/joshuarubin/go-sway go get github.com/joshuarubin/go-sway
go get github.com/allan-simon/go-singleinstance go get github.com/allan-simon/go-singleinstance
go get "github.com/sirupsen/logrus" go get "github.com/sirupsen/logrus"
go get github.com/fsnotify/fsnotify
build: build:
go build -o bin/nwg-drawer *.go go build -o bin/nwg-drawer .
install: install:
-pkill -f nwg-drawer
sleep 1
mkdir -p /usr/share/nwg-drawer mkdir -p /usr/share/nwg-drawer
cp -r desktop-directories /usr/share/nwg-drawer cp -r desktop-directories /usr/share/nwg-drawer
cp drawer.css /usr/share/nwg-drawer cp drawer.css /usr/share/nwg-drawer
@@ -21,5 +24,4 @@ uninstall:
rm /usr/bin/nwg-drawer rm /usr/bin/nwg-drawer
run: run:
go build -o bin/nwg-drawer *.go go run .
bin/nwg-drawer

View File

@@ -21,6 +21,11 @@ and `nwggrid`.
[![Packaging status](https://repology.org/badge/vertical-allrepos/nwg-drawer.svg)](https://repology.org/project/nwg-drawer/versions) [![Packaging status](https://repology.org/badge/vertical-allrepos/nwg-drawer.svg)](https://repology.org/project/nwg-drawer/versions)
## v0.2.x note
1. Placing config files in the nwg-panel config directory was a mistake, sorry. The 0.2.0 version migrates them to `~/.config/nwg-drawer`.
2. From now on you may run the program residently, which should speed it up. See "Running" below.
## Installation ## Installation
### Dependencies ### Dependencies
@@ -55,12 +60,13 @@ $ nwg-drawer -h
Usage of nwg-drawer: Usage of nwg-drawer:
-c uint -c uint
number of Columns (default 6) number of Columns (default 6)
-d Turn on Debug messages
-fm string -fm string
File Manager (default "thunar") File Manager (default "thunar")
-fscol uint -fscol uint
File Search result COLumns (default 2) File Search result COLumns (default 2)
-fslen int -fslen int
File Search name length Limit (default 80) File Search name LENgth Limit (default 80)
-is int -is int
Icon Size (default 64) Icon Size (default 64)
-lang string -lang string
@@ -73,6 +79,7 @@ Usage of nwg-drawer:
name of the Output to display the drawer on (sway only) name of the Output to display the drawer on (sway only)
-ovl -ovl
use OVerLay layer use OVerLay layer
-r Leave the program resident in memory
-s string -s string
Styling: css file name (default "drawer.css") Styling: css file name (default "drawer.css")
-spacing uint -spacing uint
@@ -84,9 +91,43 @@ Usage of nwg-drawer:
*NOTE: the `$TERM` environment variable overrides the `-term` argument if defined.* *NOTE: the `$TERM` environment variable overrides the `-term` argument if defined.*
## Running
Since v0.2.x you may use the drawer in two ways:
1. Simply run the `nwg-drawer` command, by adding a key binding to your sway config file, e.g.:
```text
bindsym Mod1+F1 exec nwg-drawer
```
2. Run a resident instance on startup, and use the `nwg-drawer` command to show the window, e.g.:
```text
exec_always nwg-drawer -r
bindsym Mod1+F1 exec nwg-drawer
```
The second line does nothing but `pkill -USR1 nwg-drawer`, so you may just use this command instead. Actually
this should be a little bit faster.
Running a resident instance should speed up use of the drawer significantly. Pay attention to the fact, that you
need to `pkill -f nwg-drawer` and reload sway to apply any new arguments!
## Logging
In case you encounter an issue, you may need debug messages. If you use the resident instance, you'll see nothing
in the terminal. Please edit your sway config file:
```text
exec nwg-drawer -r -d 2> ~/drawer.log
```
exit sway, launch it again and include the `drawer.log` content in the GitHub issue. Do not use `exec_always` here: it'll destroy the log file content on sway reload.
## Styling ## Styling
Edit `~/.config/nwg-panel/drawer.css` to your taste. Edit `~/.config/nwg-drawer/drawer.css` to your taste.
## Files ## Files
@@ -97,7 +138,7 @@ When the search phrase is at least 3 characters long, your XDG user directories
Use the **left mouse button** to open a file with the `xdg-open` command. As configuring file associations for it is 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. PITA, you may override them, by creating the `~/.config/nwg-panel/preferred-apps.json` file with your own definitions.
### Sample file content ### Sample `preferred-apps.json` file content
```json ```json
{ {
@@ -110,8 +151,8 @@ PITA, you may override them, by creating the `~/.config/nwg-panel/preferred-apps
} }
``` ```
Use the **right mouse button** to open the file with your file manager (see `-fm` argument). The result depends on the Use the **right mouse button** to open the file with your file manager (see `-fm` argument). The result depends
file manager you use. on the file manager you use.
- thunar will open the file location - thunar will open the file location
- pcmanfm will open the file with its associated program - pcmanfm will open the file with its associated program
@@ -119,6 +160,16 @@ file manager you use.
I've noy yet tried other file managers. I've noy yet tried other file managers.
### File search exclusions
You may want to exclude some paths inside your XDG user directories from searching. If so, define exclusions in the
`~/.config/nwg-panel/excluded-dirs` file, e.g. like this:
```text
# exclude all paths containing 'node_modules'
node_modules
```
## Credits ## Credits
This program uses some great libraries: This program uses some great libraries:
@@ -128,3 +179,5 @@ Copyright (c) 2015-2018 gotk3 contributors
- [gotk3-layershell](https://github.com/dlasky/gotk3-layershell) by [@dlasky](https://github.com/dlasky/gotk3-layershell/commits?author=dlasky) - many thanks for writing this software, and for patience with my requests! - [gotk3-layershell](https://github.com/dlasky/gotk3-layershell) by [@dlasky](https://github.com/dlasky/gotk3-layershell/commits?author=dlasky) - many thanks for writing this software, and for patience with my requests!
- [go-sway](https://github.com/joshuarubin/go-sway) Copyright (c) 2019 Joshua Rubin - [go-sway](https://github.com/joshuarubin/go-sway) Copyright (c) 2019 Joshua Rubin
- [go-singleinstance](github.com/allan-simon/go-singleinstance) Copyright (c) 2015 Allan Simon - [go-singleinstance](github.com/allan-simon/go-singleinstance) Copyright (c) 2015 Allan Simon
- [logrus](https://github.com/sirupsen/logrus) Copyright (c) 2014 Simon Eskildsen
- [fsnotify](https://github.com/fsnotify/fsnotify) Copyright (c) 2012-2019 fsnotify Authors

Binary file not shown.

5
go.mod
View File

@@ -4,8 +4,9 @@ go 1.16
require ( require (
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
github.com/dlasky/gotk3-layershell v0.0.0-20210331230524-5cca0b819261 github.com/dlasky/gotk3-layershell v0.0.0-20210827021656-e6ecab2731f7
github.com/gotk3/gotk3 v0.6.0 github.com/fsnotify/fsnotify v1.5.1
github.com/gotk3/gotk3 v0.6.1
github.com/joshuarubin/go-sway v0.0.4 github.com/joshuarubin/go-sway v0.0.4
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
) )

14
go.sum
View File

@@ -3,11 +3,12 @@ github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.m
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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-20210827021656-e6ecab2731f7 h1:LDo0kwt+oW9a4lWlj2OakIgMW1ySXXVRGFt8GHUdYYA=
github.com/dlasky/gotk3-layershell v0.0.0-20210331230524-5cca0b819261/go.mod h1:d56Gslp3IaiT8lqxD/lO1Msz1wYgD8D/HTKHgSdg9tU= github.com/dlasky/gotk3-layershell v0.0.0-20210827021656-e6ecab2731f7/go.mod h1:JHLx2Wz4mAPVwn4PFhC69ydwyHP4A3wQvlg7HKVVc1U=
github.com/gotk3/gotk3 v0.5.3-0.20210223154815-289cfb6dbf32/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/gotk3/gotk3 v0.6.0 h1:Aqlq4/6VabNwtCyA9M9zFNad5yHAqCi5heWnZ9y+3dA= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/gotk3/gotk3 v0.6.0/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= github.com/gotk3/gotk3 v0.6.1 h1:GJ400a0ecEEWrzjBvzBzH+pB/esEMIGdB9zPSmBdoeo=
github.com/gotk3/gotk3 v0.6.1/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/joshuarubin/go-sway v0.0.4 h1:dpmIwQ/LytG+oMrjmaVKdk1aPdW2feXK/+wAcLKIx4A= 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/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 h1:N/lPEC8f+dBZ1Tn99vShqp36LwB+LI7XNAiNadZeLUQ=
@@ -26,5 +27,6 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 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 h1:IqXQ59gzdXv58Jmm2xn0tSOR9i6HqroaOFRQ3wR/dJQ=
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

190
main.go
View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@@ -20,16 +21,16 @@ import (
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
) )
const version = "0.1.12" const version = "0.2.0"
var ( var (
appDirs []string appDirs []string
configDirectory string configDirectory string
pinnedFile string pinnedFile string
pinned []string pinned []string
src glib.SourceHandle
id2entry map[string]desktopEntry id2entry map[string]desktopEntry
preferredApps map[string]interface{} preferredApps map[string]interface{}
exclusions []string
) )
var categoryNames = [...]string{ var categoryNames = [...]string{
@@ -82,6 +83,7 @@ var desktopEntries []desktopEntry
// UI elements // UI elements
var ( var (
win *gtk.Window
resultWindow *gtk.ScrolledWindow resultWindow *gtk.ScrolledWindow
fileSearchResults []string fileSearchResults []string
searchEntry *gtk.SearchEntry searchEntry *gtk.SearchEntry
@@ -98,6 +100,9 @@ var (
statusLabel *gtk.Label statusLabel *gtk.Label
status string status string
ignore string ignore string
showWindowTrigger bool
desktopTrigger bool
pinnedTrigger bool
) )
func defaultStringIfBlank(s, fallback string) string { func defaultStringIfBlank(s, fallback string) string {
@@ -122,48 +127,79 @@ var itemSpacing = flag.Uint("spacing", 20, "icon spacing")
var lang = flag.String("lang", "", "force lang, e.g. \"en\", \"pl\"") var lang = flag.String("lang", "", "force lang, e.g. \"en\", \"pl\"")
var fileManager = flag.String("fm", "thunar", "File Manager") var fileManager = flag.String("fm", "thunar", "File Manager")
var term = flag.String("term", defaultStringIfBlank(os.Getenv("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 nameLimit = flag.Int("fslen", 80, "File Search name LENgth Limit")
var noCats = flag.Bool("nocats", false, "Disable filtering by category") var noCats = flag.Bool("nocats", false, "Disable filtering by category")
var noFS = flag.Bool("nofs", false, "Disable file search") var noFS = flag.Bool("nofs", false, "Disable file search")
var resident = flag.Bool("r", false, "Leave the program resident in memory")
var debug = flag.Bool("d", false, "Turn on Debug messages")
func main() { func main() {
timeStart := time.Now() timeStart := time.Now()
flag.Parse() flag.Parse()
if *debug {
log.SetLevel(log.DebugLevel)
}
if *displayVersion { if *displayVersion {
fmt.Printf("nwg-drawer version %s\n", version) fmt.Printf("nwg-drawer version %s\n", version)
os.Exit(0) os.Exit(0)
} }
// Gentle SIGTERM handler thanks to reiki4040 https://gist.github.com/reiki4040/be3705f307d3cd136e85 // Gentle SIGTERM handler thanks to reiki4040 https://gist.github.com/reiki4040/be3705f307d3cd136e85
// v0.2: we also need to support SIGUSR from now on
signalChan := make(chan os.Signal, 1) signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGTERM) signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGUSR1)
go func() { go func() {
for { for {
s := <-signalChan s := <-signalChan
if s == syscall.SIGTERM { switch s {
log.Info("SIGTERM received, bye bye!") case syscall.SIGTERM:
log.Info("SIGTERM received, bye bye")
gtk.MainQuit() gtk.MainQuit()
case syscall.SIGUSR1:
if *resident {
// As win.Show() called from inside a goroutine randomly crashes GTK,
// let's just set e helper variable here. We'll be checking it with glib.TimeoutAdd.
log.Debug("SIGUSR1 received, showing the window")
showWindowTrigger = true
} else {
log.Info("SIGUSR1 received, and I'm not resident, bye bye")
gtk.MainQuit()
}
default:
log.Info("Unknown signal")
} }
} }
}() }()
// We want the same key/mouse binding to turn the dock off: kill the running instance and exit. // If running instance found, we want it to show the window. The new instance will send SIGUSR1 and die
lockFilePath := fmt.Sprintf("%s/nwg-drawer.lock", tempDir()) // (equivalent of `pkill -USR1 nwg-drawer`).
// Otherwise the command may behave in two ways:
// 1. kill the running non-residennt instance and exit;
// 2. die if a resident instance found.
lockFilePath := path.Join(tempDir(), "nwg-drawer.lock")
lockFile, err := singleinstance.CreateLockFile(lockFilePath) lockFile, err := singleinstance.CreateLockFile(lockFilePath)
if err != nil { if err != nil {
pid, err := readTextFile(lockFilePath) pid, err := readTextFile(lockFilePath)
if err == nil { if err == nil {
i, err := strconv.Atoi(pid) i, err := strconv.Atoi(pid)
if err == nil { if err == nil {
log.Info("Running instance found, sending SIGTERM and exiting...") if *resident {
syscall.Kill(i, syscall.SIGTERM) log.Warnf("Resident instance already running (PID %v)", i)
} else {
log.Infof("Showing resident instance (PID %v)", i)
syscall.Kill(i, syscall.SIGUSR1)
}
} }
} }
os.Exit(0) os.Exit(0)
} }
defer lockFile.Close() defer lockFile.Close()
log.Infof("term: %s", *term)
// LANGUAGE // LANGUAGE
if *lang == "" && os.Getenv("LANG") != "" { if *lang == "" && os.Getenv("LANG") != "" {
*lang = strings.Split(os.Getenv("LANG"), ".")[0] *lang = strings.Split(os.Getenv("LANG"), ".")[0]
@@ -173,6 +209,29 @@ func main() {
// ENVIRONMENT // ENVIRONMENT
configDirectory = configDir() configDirectory = configDir()
// Placing the drawer config files in the nwg-panel config directory was a mistake.
// Let's move them to their own location.
oldConfigDirectory, err := oldConfigDir()
if err == nil {
for _, p := range []string{"drawer.css", "preferred-apps.json"} {
if pathExists(path.Join(oldConfigDirectory, p)) {
log.Infof("File %s found in stale location, moving to %s", p, configDirectory)
if !pathExists(path.Join(configDirectory, p)) {
err = os.Rename(path.Join(oldConfigDirectory, p), path.Join(configDirectory, p))
if err == nil {
log.Info("Success")
} else {
log.Warn(err)
}
} else {
log.Warnf("Failed moving %s to %s: path already exists!", path.Join(oldConfigDirectory, p), path.Join(configDirectory, p))
}
}
}
}
// Copy default style sheet if not found
if !pathExists(filepath.Join(configDirectory, "drawer.css")) { if !pathExists(filepath.Join(configDirectory, "drawer.css")) {
copyFile(filepath.Join(getDataHome(), "nwg-drawer/drawer.css"), filepath.Join(configDirectory, "drawer.css")) copyFile(filepath.Join(getDataHome(), "nwg-drawer/drawer.css"), filepath.Join(configDirectory, "drawer.css"))
} }
@@ -203,12 +262,29 @@ func main() {
// For opening files we use xdg-open. As its configuration is PITA, we may override some associations // 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. // in the ~/.config/nwg-panel/preferred-apps.json file.
paFile := filepath.Join(configDirectory, "preferred-apps.json") paFile := path.Join(configDirectory, "preferred-apps.json")
if pathExists(paFile) {
preferredApps, err = loadPreferredApps(paFile) preferredApps, err = loadPreferredApps(paFile)
if err != nil { if err != nil {
log.Error(fmt.Sprintf("Custom associations file %s not found or invalid", paFile)) log.Infof("Custom associations file %s not found or invalid", paFile)
} else { } else {
log.Info(fmt.Sprintf("Found %v associations in %s", len(preferredApps), paFile)) log.Infof("Found %v associations in %s", len(preferredApps), paFile)
}
} else {
log.Infof("%s file not found", paFile)
}
// Load user-defined paths excluded from file search
exFile := path.Join(configDirectory, "excluded-dirs")
if pathExists(exFile) {
exclusions, err = loadTextFile(exFile)
if err != nil {
log.Infof("Search exclusions file %s not found %s", exFile, err)
} else {
log.Infof("Found %v search exclusions in %s", len(exclusions), exFile)
}
} else {
log.Infof("%s file not found", exFile)
} }
// USER INTERFACE // USER INTERFACE
@@ -226,7 +302,7 @@ func main() {
gtk.AddProviderForScreen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) gtk.AddProviderForScreen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
} }
win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL) win, err = gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
if err != nil { if err != nil {
log.Fatal("Unable to create window:", err) log.Fatal("Unable to create window:", err)
} }
@@ -275,7 +351,11 @@ func main() {
searchEntry.GrabFocus() searchEntry.GrabFocus()
searchEntry.SetText("") searchEntry.SetText("")
} else { } else {
if !*resident {
gtk.MainQuit() gtk.MainQuit()
} else {
restoreStateAndHide()
}
} }
return false return false
case gdk.KEY_downarrow, gdk.KEY_Up, gdk.KEY_Down, gdk.KEY_Left, gdk.KEY_Right, gdk.KEY_Tab, case gdk.KEY_downarrow, gdk.KEY_Up, gdk.KEY_Down, gdk.KEY_Left, gdk.KEY_Right, gdk.KEY_Tab,
@@ -290,18 +370,6 @@ func main() {
} }
}) })
// Close the window on leave, but not immediately, to avoid accidental closes
win.Connect("leave-notify-event", func() {
src = glib.TimeoutAdd(uint(500), func() bool {
gtk.MainQuit()
return false
})
})
win.Connect("enter-notify-event", func() {
cancelClose()
})
/* /*
In case someone REALLY needed to use X11 - for some stupid Zoom meeting or something, this allows 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. the drawer to behave properly on Openbox, and possibly somewhere else. For sure not on i3.
@@ -341,13 +409,15 @@ func main() {
resultWindow, _ = gtk.ScrolledWindowNew(nil, nil) resultWindow, _ = gtk.ScrolledWindowNew(nil, nil)
resultWindow.SetEvents(int(gdk.ALL_EVENTS_MASK)) resultWindow.SetEvents(int(gdk.ALL_EVENTS_MASK))
resultWindow.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) 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 { resultWindow.Connect("button-release-event", func(sw *gtk.ScrolledWindow, e *gdk.Event) bool {
btnEvent := gdk.EventButtonNewFromEvent(e) btnEvent := gdk.EventButtonNewFromEvent(e)
if btnEvent.Button() == 1 || btnEvent.Button() == 3 { if btnEvent.Button() == 1 || btnEvent.Button() == 3 {
if !*resident {
gtk.MainQuit() gtk.MainQuit()
} else {
restoreStateAndHide()
}
return true return true
} }
return false return false
@@ -392,6 +462,7 @@ func main() {
statusLineWrapper.PackStart(statusLabel, true, false, 0) statusLineWrapper.PackStart(statusLabel, true, false, 0)
win.ShowAll() win.ShowAll()
if !*noFS { if !*noFS {
fileSearchResultWrapper.SetSizeRequest(appFlowBox.GetAllocatedWidth(), 1) fileSearchResultWrapper.SetSizeRequest(appFlowBox.GetAllocatedWidth(), 1)
fileSearchResultWrapper.Hide() fileSearchResultWrapper.Hide()
@@ -399,8 +470,69 @@ func main() {
if !*noCats { if !*noCats {
categoriesWrapper.SetSizeRequest(1, categoriesWrapper.GetAllocatedHeight()*2) categoriesWrapper.SetSizeRequest(1, categoriesWrapper.GetAllocatedHeight()*2)
} }
if *resident {
win.Hide()
}
t := time.Now() t := time.Now()
log.Info(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()))
// Check if showing the window has been requested (SIGUSR1)
glib.TimeoutAdd(uint(1), func() bool {
if showWindowTrigger && win != nil && !win.IsVisible() {
win.ShowAll()
// focus 1st element
b := appFlowBox.GetChildAtIndex(0)
if b != nil {
button, err := b.GetChild()
if err == nil {
button.ToWidget().GrabFocus()
}
}
}
showWindowTrigger = false
// some .desktop file changed
if desktopTrigger {
log.Debug(".desktop file changed")
desktopFiles = listDesktopFiles()
status = parseDesktopFiles(desktopFiles)
appFlowBox = setUpAppsFlowBox(nil, "")
desktopTrigger = false
}
// pinned file changed
if pinnedTrigger {
log.Debug("pinned file changed")
pinnedTrigger = false
pinned, _ = loadTextFile(pinnedFile)
pinnedFlowBox = setUpPinnedFlowBox()
}
return true
})
go watchFiles()
gtk.Main() gtk.Main()
} }
func restoreStateAndHide() {
timeStart1 := time.Now()
win.Hide()
// clear search
searchEntry.SetText("")
// clear category filter (in gotk3 it means: rebuild, as we have no filtering here)
appFlowBox = setUpAppsFlowBox(nil, "")
for _, btn := range catButtons {
btn.SetImagePosition(gtk.POS_LEFT)
btn.SetSizeRequest(0, 0)
}
// scroll to the top
resultWindow.GetVAdjustment().SetValue(0)
t := time.Now()
log.Debugf(fmt.Sprintf("UI hidden and restored in the backgroud in %v ms", t.Sub(timeStart1).Milliseconds()))
}

View File

@@ -10,6 +10,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort" "sort"
@@ -19,7 +20,6 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/joshuarubin/go-sway" "github.com/joshuarubin/go-sway"
) )
@@ -28,19 +28,6 @@ func wayland() bool {
return os.Getenv("WAYLAND_DISPLAY") != "" || os.Getenv("XDG_SESSION_TYPE") == "wayland" 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.
Furthermore - hovering a widget triggers window leave-notify-event event, and the timeout
needs to be cleared as well.
*/
func cancelClose() {
if src > 0 {
glib.SourceRemove(src)
src = 0
}
}
func createPixbuf(icon string, size int) (*gdk.Pixbuf, error) { func createPixbuf(icon string, size int) (*gdk.Pixbuf, error) {
iconTheme, err := gtk.IconThemeGetDefault() iconTheme, err := gtk.IconThemeGetDefault()
if err != nil { if err != nil {
@@ -163,14 +150,29 @@ func readTextFile(path string) (string, error) {
return string(bytes), nil return string(bytes), nil
} }
func configDir() string { func oldConfigDir() (string, error) {
if os.Getenv("XDG_CONFIG_HOME") != "" { if os.Getenv("XDG_CONFIG_HOME") != "" {
dir := fmt.Sprintf("%s/nwg-panel", os.Getenv("XDG_CONFIG_HOME")) dir := path.Join(os.Getenv("XDG_CONFIG_HOME"), "nwg-panel")
createDir(dir) return dir, nil
return (fmt.Sprintf("%s/nwg-panel", os.Getenv("XDG_CONFIG_HOME"))) } else if os.Getenv("HOME") != "" {
dir := path.Join(os.Getenv("HOME"), ".config/nwg-panel")
return dir, nil
} }
dir := fmt.Sprintf("%s/.config/nwg-panel", os.Getenv("HOME"))
return "", errors.New("old config dir not found")
}
func configDir() string {
var dir string
if os.Getenv("XDG_CONFIG_HOME") != "" {
dir = path.Join(os.Getenv("XDG_CONFIG_HOME"), "nwg-drawer")
} else if os.Getenv("HOME") != "" {
dir = path.Join(os.Getenv("HOME"), ".config/nwg-drawer")
}
log.Infof("Config dir: %s", dir)
createDir(dir) createDir(dir)
return dir return dir
} }
@@ -240,11 +242,17 @@ func getAppDirs() []string {
"/var/lib/flatpak/exports/share/applications"} "/var/lib/flatpak/exports/share/applications"}
for _, d := range flatpakDirs { for _, d := range flatpakDirs {
if !isIn(dirs, d) { if pathExists(d) && !isIn(dirs, d) {
dirs = append(dirs, d) dirs = append(dirs, d)
} }
} }
return dirs var confirmedDirs []string
for _, d := range dirs {
if pathExists(d) {
confirmedDirs = append(confirmedDirs, d)
}
}
return confirmedDirs
} }
func loadPreferredApps(path string) (map[string]interface{}, error) { func loadPreferredApps(path string) (map[string]interface{}, error) {
@@ -350,6 +358,7 @@ func setUpCategories() {
} }
func parseDesktopFiles(desktopFiles []string) string { func parseDesktopFiles(desktopFiles []string) string {
desktopEntries = nil
id2entry = make(map[string]desktopEntry) id2entry = make(map[string]desktopEntry)
skipped := 0 skipped := 0
hidden := 0 hidden := 0
@@ -368,7 +377,7 @@ func parseDesktopFiles(desktopFiles []string) string {
if entry.NoDisplay { if entry.NoDisplay {
hidden++ hidden++
// We still need hidden entries, so `continue` is disallowed here // We still need hidden entries, so `continue` is disallowed here
// Fixes introduced in #19 // Fixes bug introduced in #19
} }
id2entry[entry.DesktopID] = entry id2entry[entry.DesktopID] = entry
@@ -461,7 +470,7 @@ func loadTextFile(path string) ([]string, error) {
var output []string var output []string
for _, line := range lines { for _, line := range lines {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if line != "" { if line != "" && !strings.HasPrefix(line, "#") {
output = append(output, line) output = append(output, line)
} }
@@ -563,12 +572,13 @@ func launch(command string, terminal bool) {
msg := fmt.Sprintf("env vars: %s; command: '%s'; args: %s\n", envVars, elements[cmdIdx], elements[1+cmdIdx:]) msg := fmt.Sprintf("env vars: %s; command: '%s'; args: %s\n", envVars, elements[cmdIdx], elements[1+cmdIdx:])
log.Info(msg) log.Info(msg)
go cmd.Run() cmd.Start()
glib.TimeoutAdd(uint(150), func() bool { if *resident {
restoreStateAndHide()
} else {
gtk.MainQuit() gtk.MainQuit()
return false }
})
} }
func open(filePath string, xdgOpen bool) { func open(filePath string, xdgOpen bool) {
@@ -586,10 +596,15 @@ func open(filePath string, xdgOpen bool) {
} else { } else {
cmd = exec.Command(*fileManager, filePath) cmd = exec.Command(*fileManager, filePath)
} }
fmt.Printf("Executing: %s", cmd) log.Infof("Executing: %s", cmd)
cmd.Start() cmd.Start()
if *resident {
restoreStateAndHide()
} else {
gtk.MainQuit() gtk.MainQuit()
}
} }
// Returns map output name -> gdk.Monitor // Returns map output name -> gdk.Monitor

View File

@@ -7,6 +7,7 @@ import (
"strings" "strings"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
) )
@@ -34,19 +35,13 @@ func setUpPinnedFlowBox() *gtk.FlowBox {
btn, _ := gtk.ButtonNew() btn, _ := gtk.ButtonNew()
var pixbuf *gdk.Pixbuf
var img *gtk.Image var img *gtk.Image
var err error
if entry.Icon != "" { if entry.Icon != "" {
pixbuf, err = createPixbuf(entry.Icon, *iconSize) pixbuf, _ := createPixbuf(entry.Icon, *iconSize)
} else {
pixbuf, err = createPixbuf("image-missing", *iconSize)
}
if err != nil {
log.Error(err)
pixbuf, _ = createPixbuf("unknown", *iconSize)
}
img, _ = gtk.ImageNewFromPixbuf(pixbuf) img, _ = gtk.ImageNewFromPixbuf(pixbuf)
} else {
img, _ = gtk.ImageNewFromIconName("image-missing", gtk.ICON_SIZE_INVALID)
}
btn.SetImage(img) btn.SetImage(img)
btn.SetAlwaysShowImage(true) btn.SetAlwaysShowImage(true)
@@ -72,7 +67,6 @@ func setUpPinnedFlowBox() *gtk.FlowBox {
return true return true
} else if btnEvent.Button() == 3 { } else if btnEvent.Button() == 3 {
unpinItem(entry.DesktopID) unpinItem(entry.DesktopID)
pinnedFlowBox = setUpPinnedFlowBox()
return true return true
} }
return false return false
@@ -92,10 +86,6 @@ func setUpPinnedFlowBox() *gtk.FlowBox {
item.(*gtk.Widget).SetCanFocus(false) item.(*gtk.Widget).SetCanFocus(false)
}) })
} }
flowBox.Connect("enter-notify-event", func() {
cancelClose()
})
flowBox.ShowAll() flowBox.ShowAll()
return flowBox return flowBox
@@ -115,9 +105,7 @@ func setUpCategoriesButtonBox() *gtk.EventBox {
} }
eventBox, _ := gtk.EventBoxNew() eventBox, _ := gtk.EventBoxNew()
eventBox.Connect("enter-notify-event", func() {
cancelClose()
})
hBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) hBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
eventBox.Add(hBox) eventBox.Add(hBox)
button, _ := gtk.ButtonNewWithLabel("All") button, _ := gtk.ButtonNewWithLabel("All")
@@ -138,7 +126,6 @@ func setUpCategoriesButtonBox() *gtk.EventBox {
button.SetProperty("name", "category-button") button.SetProperty("name", "category-button")
catButtons = append(catButtons, button) catButtons = append(catButtons, button)
button.SetLabel(cat.DisplayName) button.SetLabel(cat.DisplayName)
// fix #8
button.SetAlwaysShowImage(true) button.SetAlwaysShowImage(true)
hBox.PackStart(button, false, false, 0) hBox.PackStart(button, false, false, 0)
name := cat.Name name := cat.Name
@@ -246,10 +233,10 @@ func flowBoxButton(entry desktopEntry) *gtk.Button {
if entry.Icon != "" { if entry.Icon != "" {
pixbuf, err = createPixbuf(entry.Icon, *iconSize) pixbuf, err = createPixbuf(entry.Icon, *iconSize)
} else { } else {
log.Warnf("Undefined icon for %s", entry.Name)
pixbuf, err = createPixbuf("image-missing", *iconSize) pixbuf, err = createPixbuf("image-missing", *iconSize)
} }
if err != nil { if err != nil {
log.Error(err)
pixbuf, _ = createPixbuf("unknown", *iconSize) pixbuf, _ = createPixbuf("unknown", *iconSize)
} }
img, _ = gtk.ImageNewFromPixbuf(pixbuf) img, _ = gtk.ImageNewFromPixbuf(pixbuf)
@@ -275,7 +262,6 @@ func flowBoxButton(entry desktopEntry) *gtk.Button {
return true return true
} else if btnEvent.Button() == 3 { } else if btnEvent.Button() == 3 {
pinItem(ID) pinItem(ID)
pinnedFlowBox = setUpPinnedFlowBox()
return true return true
} }
return false return false
@@ -295,9 +281,6 @@ func setUpFileSearchResultContainer() *gtk.FlowBox {
} }
flowBox, _ := gtk.FlowBoxNew() flowBox, _ := gtk.FlowBoxNew()
flowBox.SetProperty("orientation", gtk.ORIENTATION_VERTICAL) flowBox.SetProperty("orientation", gtk.ORIENTATION_VERTICAL)
flowBox.Connect("enter-notify-event", func() {
cancelClose()
})
fileSearchResultWrapper.PackStart(flowBox, false, false, 10) fileSearchResultWrapper.PackStart(flowBox, false, false, 10)
return flowBox return flowBox
@@ -309,7 +292,19 @@ func walk(path string, d fs.DirEntry, e error) error {
} }
// don't search leading part of the path, as e.g. '/home/user/Pictures' // don't search leading part of the path, as e.g. '/home/user/Pictures'
toSearch := strings.Split(path, ignore)[1] toSearch := strings.Split(path, ignore)[1]
if strings.Contains(strings.ToLower(toSearch), strings.ToLower(phrase)) {
// Remaing part of the path (w/o file name) must be checked against being present in excluded dirs
doSearch := true
parts := strings.Split(toSearch, "/")
remainingPart := ""
if len(parts) > 1 {
remainingPart = strings.Join(parts[:len(parts)-1], "/")
}
if remainingPart != "" && isExcluded(remainingPart) {
doSearch = false
}
if doSearch && strings.Contains(strings.ToLower(toSearch), strings.ToLower(phrase)) {
// mark directories // mark directories
if d.IsDir() { if d.IsDir() {
fileSearchResults = append(fileSearchResults, fmt.Sprintf("#is_dir#%s", path)) fileSearchResults = append(fileSearchResults, fmt.Sprintf("#is_dir#%s", path))
@@ -317,15 +312,16 @@ func walk(path string, d fs.DirEntry, e error) error {
fileSearchResults = append(fileSearchResults, path) fileSearchResults = append(fileSearchResults, path)
} }
} }
return nil return nil
} }
func setUpSearchEntry() *gtk.SearchEntry { func setUpSearchEntry() *gtk.SearchEntry {
searchEntry, _ := gtk.SearchEntryNew() searchEntry, _ := gtk.SearchEntryNew()
searchEntry.SetPlaceholderText("Type to search") searchEntry.SetPlaceholderText("Type to search")
searchEntry.Connect("enter-notify-event", func() { /*searchEntry.Connect("enter-notify-event", func() {
cancelClose() cancelClose()
}) })*/
searchEntry.Connect("search-changed", func() { searchEntry.Connect("search-changed", func() {
for _, btn := range catButtons { for _, btn := range catButtons {
btn.SetImagePosition(gtk.POS_LEFT) btn.SetImagePosition(gtk.POS_LEFT)
@@ -380,7 +376,6 @@ func setUpSearchEntry() *gtk.SearchEntry {
if w == nil && fileSearchResultFlowBox != nil { if w == nil && fileSearchResultFlowBox != nil {
f := fileSearchResultFlowBox.GetChildAtIndex(0) f := fileSearchResultFlowBox.GetChildAtIndex(0)
if f != nil { if f != nil {
//f.SetCanFocus(false)
button, err := f.GetChild() button, err := f.GetChild()
if err == nil { if err == nil {
button.ToWidget().SetCanFocus(true) button.ToWidget().SetCanFocus(true)
@@ -401,13 +396,19 @@ func setUpSearchEntry() *gtk.SearchEntry {
} }
} }
}) })
/*searchEntry.Connect("focus-in-event", func() {
searchEntry.SetText("")
})*/
return searchEntry return searchEntry
} }
func isExcluded(dir string) bool {
for _, exclusion := range exclusions {
if strings.Contains(dir, exclusion) {
return true
}
}
return false
}
func searchUserDir(dir string) { func searchUserDir(dir string) {
fileSearchResults = nil fileSearchResults = nil
ignore = userDirsMap[dir] ignore = userDirsMap[dir]
@@ -420,9 +421,12 @@ func searchUserDir(dir string) {
for _, path := range fileSearchResults { for _, path := range fileSearchResults {
partOfPathToShow := strings.Split(path, userDirsMap[dir])[1] partOfPathToShow := strings.Split(path, userDirsMap[dir])[1]
if partOfPathToShow != "" { if partOfPathToShow != "" {
if !(strings.HasPrefix(path, "#is_dir#") && isExcluded(path)) {
btn := setUpUserFileSearchResultButton(partOfPathToShow, path) btn := setUpUserFileSearchResultButton(partOfPathToShow, path)
fileSearchResultFlowBox.Add(btn) fileSearchResultFlowBox.Add(btn)
} }
}
} }
fileSearchResultFlowBox.Hide() fileSearchResultFlowBox.Hide()

62
watcher.go Normal file
View File

@@ -0,0 +1,62 @@
package main
import (
"os"
"path/filepath"
"strings"
log "github.com/sirupsen/logrus"
"github.com/fsnotify/fsnotify"
)
// Thanks to Steve Domino https://medium.com/@skdomino/watch-this-file-watching-in-go-5b5a247cf71f
var watcher *fsnotify.Watcher
func watchFiles() {
// creates a new file watcher
watcher, _ = fsnotify.NewWatcher()
defer watcher.Close()
if err := watcher.Add(pinnedFile); err != nil {
log.Errorf("ERROR", err)
}
for _, fp := range appDirs {
if err := filepath.Walk(fp, watchDir); err != nil {
log.Errorf("ERROR", err)
}
}
done := make(chan bool)
go func() {
for {
select {
case event := <-watcher.Events:
if strings.HasSuffix(event.Name, ".desktop") &&
(event.Op.String() == "CREATE" ||
event.Op.String() == "REMOVE" ||
event.Op.String() == "RENAME") {
desktopTrigger = true
} else if event.Name == pinnedFile {
pinnedTrigger = true
}
case err := <-watcher.Errors:
log.Errorf("ERROR", err)
}
}
}()
<-done
}
func watchDir(path string, fi os.FileInfo, err error) error {
if fi.Mode().IsDir() {
return watcher.Add(path)
}
return nil
}