17 Commits

Author SHA1 Message Date
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
8 changed files with 205 additions and 104 deletions

View File

@@ -20,4 +20,5 @@ uninstall:
rm /usr/bin/nwg-drawer rm /usr/bin/nwg-drawer
run: run:
go run *.go go build -o bin/nwg-drawer *.go
bin/nwg-drawer

View File

@@ -82,6 +82,8 @@ Usage of nwg-drawer:
-v display Version information -v display Version information
``` ```
*NOTE: the `$TERM` environment variable overrides the `-term` argument if defined.*
## Styling ## Styling
Edit `~/.config/nwg-panel/drawer.css` to your taste. Edit `~/.config/nwg-panel/drawer.css` to your taste.

Binary file not shown.

22
main.go
View File

@@ -19,7 +19,7 @@ import (
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
) )
const version = "0.1.7" const version = "0.1.10"
var ( var (
appDirs []string appDirs []string
@@ -59,6 +59,7 @@ type desktopEntry struct {
CommentLoc string CommentLoc string
Icon string Icon string
Exec string Exec string
Category string
Terminal bool Terminal bool
NoDisplay bool NoDisplay bool
} }
@@ -98,6 +99,16 @@ var (
ignore 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 // Flags
var cssFileName = flag.String("s", "drawer.css", "Styling: css file name") var cssFileName = flag.String("s", "drawer.css", "Styling: css file name")
var targetOutput = flag.String("o", "", "name of the Output to display the drawer on (sway only)") var targetOutput = flag.String("o", "", "name of the Output to display the drawer on (sway only)")
@@ -109,7 +120,7 @@ var columnsNumber = flag.Uint("c", 6, "number of Columns")
var itemSpacing = flag.Uint("spacing", 20, "icon spacing") 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", "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")
@@ -197,7 +208,6 @@ func main() {
println(fmt.Sprintf("Custom associations file %s not found or invalid", paFile)) println(fmt.Sprintf("Custom associations file %s not found or invalid", paFile))
} else { } else {
println(fmt.Sprintf("Found %v associations in %s", len(preferredApps), paFile)) println(fmt.Sprintf("Found %v associations in %s", len(preferredApps), paFile))
fmt.Println(preferredApps)
} }
// USER INTERFACE // USER INTERFACE
@@ -208,7 +218,7 @@ func main() {
err = cssProvider.LoadFromPath(cssFile) err = cssProvider.LoadFromPath(cssFile)
if err != nil { if err != nil {
println(fmt.Sprintf("ERROR: %s css file not found or erroneous. Using GTK styling.", cssFile)) println(fmt.Sprintf("ERROR: %s css file not found or erroneous. Using GTK styling.", cssFile))
println(fmt.Sprintf(">>> %s", err)) println(fmt.Sprintf("%s", err))
} else { } else {
println(fmt.Sprintf("Using style from %s", cssFile)) println(fmt.Sprintf("Using style from %s", cssFile))
screen, _ := gdk.ScreenGetDefault() screen, _ := gdk.ScreenGetDefault()
@@ -232,7 +242,7 @@ func main() {
layershell.SetMonitor(win, monitor) layershell.SetMonitor(win, monitor)
} else { } else {
println(err) println(fmt.Sprintf("%s", err))
} }
} }
@@ -273,7 +283,7 @@ func main() {
default: default:
if !searchEntry.IsFocus() { if !searchEntry.IsFocus() {
searchEntry.GrabFocus() searchEntry.GrabFocusWithoutSelecting()
} }
return false return false
} }

114
tools.go
View File

@@ -49,7 +49,7 @@ func createPixbuf(icon string, size int) (*gdk.Pixbuf, error) {
if strings.Contains(icon, "/") { if strings.Contains(icon, "/") {
pixbuf, err := gdk.PixbufNewFromFileAtSize(icon, size, size) pixbuf, err := gdk.PixbufNewFromFileAtSize(icon, size, size)
if err != nil { if err != nil {
println(err) println(fmt.Sprintf("%s", err))
return nil, err return nil, err
} }
return pixbuf, nil return pixbuf, nil
@@ -350,103 +350,29 @@ func setUpCategories() {
func parseDesktopFiles(desktopFiles []string) string { func parseDesktopFiles(desktopFiles []string) string {
id2entry = make(map[string]desktopEntry) id2entry = make(map[string]desktopEntry)
var added []string
skipped := 0 skipped := 0
hidden := 0 hidden := 0
for _, file := range desktopFiles { for _, file := range desktopFiles {
lines, err := loadTextFile(file) id := filepath.Base(file)
if err == nil { if _, ok := id2entry[id]; ok {
parts := strings.Split(file, "/") skipped++
desktopID := parts[len(parts)-1] continue
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 {
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++
}
} }
entry, err := parseDesktopEntryFile(id, file)
if err != nil {
continue
}
if entry.NoDisplay {
hidden++
// We still need hidden entries, so `continue` is disallowed here
// Fixes introduced in #19
}
id2entry[entry.DesktopID] = entry
desktopEntries = append(desktopEntries, entry)
assignToLists(entry.DesktopID, entry.Category)
} }
sort.Slice(desktopEntries, func(i, j int) bool { sort.Slice(desktopEntries, func(i, j int) bool {
return desktopEntries[i].NameLoc < desktopEntries[j].NameLoc return desktopEntries[i].NameLoc < desktopEntries[j].NameLoc

View File

@@ -146,7 +146,9 @@ func setUpCategoriesButtonBox() *gtk.EventBox {
w := b.GetAllocatedWidth() w := b.GetAllocatedWidth()
b.SetImagePosition(gtk.POS_TOP) b.SetImagePosition(gtk.POS_TOP)
b.SetSizeRequest(w, 0) b.SetSizeRequest(w, 0)
fileSearchResultWrapper.Hide() if fileSearchResultWrapper != nil {
fileSearchResultWrapper.Hide()
}
}) })
} }
} }
@@ -349,6 +351,29 @@ func setUpSearchEntry() *gtk.SearchEntry {
fileSearchResultWrapper.Hide() 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 { } else {
// clear search results // clear search results
appFlowBox = setUpAppsFlowBox(nil, "") appFlowBox = setUpAppsFlowBox(nil, "")
@@ -362,9 +387,9 @@ func setUpSearchEntry() *gtk.SearchEntry {
} }
} }
}) })
searchEntry.Connect("focus-in-event", func() { /*searchEntry.Connect("focus-in-event", func() {
searchEntry.SetText("") searchEntry.SetText("")
}) })*/
return searchEntry return searchEntry
} }
@@ -407,6 +432,7 @@ func setUpUserDirButton(iconName, displayName, entryName string, userDirsMap map
} }
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
button, _ := gtk.ButtonNew() button, _ := gtk.ButtonNew()
button.SetAlwaysShowImage(true)
img, _ := gtk.ImageNewFromIconName(iconName, gtk.ICON_SIZE_MENU) img, _ := gtk.ImageNewFromIconName(iconName, gtk.ICON_SIZE_MENU)
button.SetImage(img) button.SetImage(img)
@@ -439,6 +465,7 @@ func setUpUserFileSearchResultButton(fileName, filePath string) *gtk.Box {
if strings.HasPrefix(filePath, "#is_dir#") { if strings.HasPrefix(filePath, "#is_dir#") {
filePath = filePath[8:] filePath = filePath[8:]
img, _ := gtk.ImageNewFromIconName("folder", gtk.ICON_SIZE_MENU) img, _ := gtk.ImageNewFromIconName("folder", gtk.ICON_SIZE_MENU)
button.SetAlwaysShowImage(true)
button.SetImage(img) button.SetImage(img)
} }
@@ -464,6 +491,10 @@ func setUpUserFileSearchResultButton(fileName, filePath string) *gtk.Box {
return false return false
}) })
button.Connect("activate", func() {
open(filePath, true)
})
box.PackStart(button, false, true, 0) box.PackStart(button, false, true, 0)
return box return box
} }

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")
}
}