Skip to content

Commit

Permalink
improve parsing of desktop entries.
Browse files Browse the repository at this point in the history
- speed up parsing by not creating nearly as many strings.
- fix basic whitespace handling. fixes nwg-piotr#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
  • Loading branch information
james-lawrence committed Aug 31, 2021
1 parent c5e39f1 commit 57826c0
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 91 deletions.
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type desktopEntry struct {
CommentLoc string
Icon string
Exec string
Category string
Terminal bool
NoDisplay bool
}
Expand Down
107 changes: 16 additions & 91 deletions tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,103 +350,28 @@ 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

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
id := filepath.Base(file)
if _, ok := id2entry[id]; ok {
skipped++
continue
}

assignToLists(entry.DesktopID, categories)
entry, err := parseDesktopEntryFile(id, file)
if err != nil {
continue
}

} else {
skipped++
}
if entry.NoDisplay {
hidden++
continue
}

id2entry[entry.DesktopID] = entry
desktopEntries = append(desktopEntries, entry)
assignToLists(entry.DesktopID, entry.Category)
}
sort.Slice(desktopEntries, func(i, j int) bool {
return desktopEntries[i].NameLoc < desktopEntries[j].NameLoc
Expand Down
166 changes: 166 additions & 0 deletions xdgdesktop_parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package main

import (
"bufio"
"fmt"
"io"
"os"
"strconv"
"strings"
)

func parseDesktopEntryFileDeprecated(id string, path string) (entry desktopEntry, err error) {
lines, err := loadTextFile(path)
if err != nil {
return entry, err
}

return parseDesktopEntryDeprecated(id, lines)
}

func parseDesktopEntryDeprecated(id string, lines []string) (entry desktopEntry, err error) {
desktopID := id
name := ""
nameLoc := ""
comment := ""
commentLoc := ""
icon := ""
exec := ""
categories := ""
terminal := false
noDisplay := false

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
continue
}
}

// if name[ln] not found, let's try to find name[ln_LN]
if nameLoc == "" {
nameLoc = name
}
if commentLoc == "" {
commentLoc = comment
}

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
entry.Category = categories
return entry, nil
}

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, ""
}
62 changes: 62 additions & 0 deletions xdgdesktop_parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package main

import (
"strings"
"testing"
)

var result desktopEntry

func BenchmarkDesktopEntryParserOld(b *testing.B) {
var entry desktopEntry
for n := 0; n < b.N; n++ {
entry, _ = parseDesktopEntryFileDeprecated("id", "./desktop-directories/game.directory")
}
result = entry
}

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`

// entry, err := parseDesktopEntryDeprecated("id", strings.Split(whitespace, "\n"))
*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")
}
}

0 comments on commit 57826c0

Please sign in to comment.