From 57826c064d6d2412b97cb98eeee0c2a9c7855fdc Mon Sep 17 00:00:00 2001 From: James Lawrence Date: Tue, 31 Aug 2021 06:06:56 -0400 Subject: [PATCH] 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 --- main.go | 1 + tools.go | 107 ++++-------------------- xdgdesktop_parser.go | 166 ++++++++++++++++++++++++++++++++++++++ xdgdesktop_parser_test.go | 62 ++++++++++++++ 4 files changed, 245 insertions(+), 91 deletions(-) create mode 100644 xdgdesktop_parser.go create mode 100644 xdgdesktop_parser_test.go diff --git a/main.go b/main.go index 63cfbe2..1011894 100644 --- a/main.go +++ b/main.go @@ -59,6 +59,7 @@ type desktopEntry struct { CommentLoc string Icon string Exec string + Category string Terminal bool NoDisplay bool } diff --git a/tools.go b/tools.go index ee4e9f2..9e49821 100644 --- a/tools.go +++ b/tools.go @@ -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 diff --git a/xdgdesktop_parser.go b/xdgdesktop_parser.go new file mode 100644 index 0000000..68d12c9 --- /dev/null +++ b/xdgdesktop_parser.go @@ -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, "" +} diff --git a/xdgdesktop_parser_test.go b/xdgdesktop_parser_test.go new file mode 100644 index 0000000..f112508 --- /dev/null +++ b/xdgdesktop_parser_test.go @@ -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") + } +}