Initial commit

This commit is contained in:
Trevor Slocum 2019-07-14 19:27:02 -07:00
commit 6970196048
13 changed files with 720 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.idea/
dist/
*.sh
vendor/
old.txt
new.txt

10
README.md Normal file
View File

@ -0,0 +1,10 @@
# desktop
[![GoDoc](https://godoc.org/git.sr.ht/~tslocum/desktop?status.svg)](https://godoc.org/git.sr.ht/~tslocum/desktop)
[![builds.sr.ht status](https://builds.sr.ht/~tslocum/desktop.svg)](https://builds.sr.ht/~tslocum/desktop)
[![Donate](https://img.shields.io/liberapay/receives/rocketnine.space.svg?logo=liberapay)](https://liberapay.com/rocketnine.space)
Desktop application library
## Warning: Experimental
Linux is the only supported platform. Windows support is planned.

37
dirs.go Normal file
View File

@ -0,0 +1,37 @@
package desktop
import (
"os"
"path"
"path/filepath"
"strings"
)
func DataDirs() []string {
var dataDirs []string
homeDir := os.Getenv("HOME")
if strings.TrimSpace(homeDir) == "" {
homeDir = "~/"
}
dataHomeSetting := os.Getenv("XDG_DATA_HOME")
if dataHomeSetting == "" {
dataHomeSetting = path.Join(homeDir, ".local/share")
}
dataDirs = append(dataDirs, filepath.Join(dataHomeSetting, "applications"))
dataDirsSetting := strings.Split(os.Getenv("XDG_DATA_DIRS"), ":")
for _, dataDir := range dataDirsSetting {
dataDir = strings.TrimSpace(dataDir)
if dataDir == "" {
continue
}
dataDirs = append(dataDirs, filepath.Join(dataDir, "applications"))
}
if len(dataDirs) == 1 {
dataDirs = append(dataDirs, "/usr/local/share/applications", "/usr/share/applications")
}
return dataDirs
}

137
entry.go Normal file
View File

@ -0,0 +1,137 @@
package desktop
import (
"bufio"
"bytes"
"fmt"
"io"
"strings"
"github.com/pkg/errors"
)
var (
entryHeader = []byte("[desktop entry]")
entryName = []byte("name=")
entryGenericName = []byte("genericname=")
entryComment = []byte("comment=")
entryIcon = []byte("icon=")
entryPath = []byte("path=")
entryExec = []byte("exec=")
entryTerminal = []byte("terminal=true")
entryNoDisplay = []byte("nodisplay=true")
entryHidden = []byte("hidden=true")
)
var quotes = map[string]string{
`%%`: `%`,
`\\\\ `: `\\ `,
`\\\\` + "`": `\\` + "`",
`\\\\$`: `\\$`,
`\\\\(`: `\\(`,
`\\\\)`: `\\)`,
`\\\\\`: `\\\`,
`\\\\\\\\`: `\\\\`,
}
func UnquoteExec(ex string) string {
for qs, qr := range quotes {
ex = strings.ReplaceAll(ex, qs, qr)
}
return ex
}
type Entry struct {
Name string
GenericName string
Comment string
Icon string
Path string
Exec string
Terminal bool
}
func (e *Entry) String() string {
name := ""
if e.Name != "" {
name = e.Name
}
if e.GenericName != "" {
if name != "" {
name += " / "
}
name += e.GenericName
}
comment := "no comment"
if e.Comment != "" {
comment = e.Comment
}
return fmt.Sprintf("{%s - %s}", name, comment)
}
func (e *Entry) ExpandExec(args string) string {
ex := e.Exec
ex = strings.ReplaceAll(ex, "%F", args)
ex = strings.ReplaceAll(ex, "%f", args)
ex = strings.ReplaceAll(ex, "%U", args)
ex = strings.ReplaceAll(ex, "%u", args)
return ex
}
func Parse(content io.Reader) (*Entry, error) {
var (
scanner = bufio.NewScanner(content)
scannedBytes []byte
scannedBytesLen int
entry = &Entry{}
foundHeader bool
)
for scanner.Scan() {
scannedBytes = bytes.TrimSpace(scanner.Bytes())
scannedBytesLen = len(scannedBytes)
if scannedBytesLen == 0 || scannedBytes[0] == byte('#') {
continue
} else if scannedBytes[0] == byte('[') {
if !foundHeader {
if scannedBytesLen < 15 || !bytes.EqualFold(scannedBytes[0:15], entryHeader) {
return nil, errors.New("invalid desktop entry: section header not found")
}
foundHeader = true
} else {
break // Start of new section
}
} else if scannedBytesLen >= 6 && bytes.EqualFold(scannedBytes[0:5], entryName) {
entry.Name = string(scannedBytes[5:])
} else if scannedBytesLen >= 13 && bytes.EqualFold(scannedBytes[0:12], entryGenericName) {
entry.GenericName = string(scannedBytes[12:])
} else if scannedBytesLen >= 9 && bytes.EqualFold(scannedBytes[0:8], entryComment) {
entry.Comment = string(scannedBytes[8:])
} else if scannedBytesLen >= 6 && bytes.EqualFold(scannedBytes[0:5], entryIcon) {
entry.Icon = string(scannedBytes[5:])
} else if scannedBytesLen >= 6 && bytes.EqualFold(scannedBytes[0:5], entryPath) {
entry.Path = string(scannedBytes[5:])
} else if scannedBytesLen >= 6 && bytes.EqualFold(scannedBytes[0:5], entryExec) {
entry.Exec = UnquoteExec(string(scannedBytes[5:]))
} else if scannedBytesLen == 13 && bytes.EqualFold(scannedBytes, entryTerminal) {
entry.Terminal = true
} else if (scannedBytesLen == 14 && bytes.EqualFold(scannedBytes, entryNoDisplay)) || (scannedBytesLen == 11 && bytes.EqualFold(scannedBytes, entryHidden)) {
return nil, nil
}
}
if err := scanner.Err(); err != nil {
return nil, errors.Wrap(err, "failed to parse desktop entry")
} else if !foundHeader {
return nil, errors.Wrap(err, "invalid desktop entry")
}
return entry, nil
}

107
entry_test.go Normal file
View File

@ -0,0 +1,107 @@
package desktop
import (
"io"
"os"
"testing"
)
type testData struct {
Filename string
Entry *Entry
}
var testCases = []*testData{
{Filename: "alacritty.desktop", Entry: &Entry{Name: "Alacritty", GenericName: "Terminal", Comment: "A cross-platform, GPU enhanced terminal emulator", Icon: "Alacritty", Path: "/home/test", Exec: "alacritty", Terminal: false}},
{Filename: "vim.desktop", Entry: &Entry{Name: "Vim", GenericName: "Text Editor", Comment: "Edit text files", Icon: "gvim", Exec: "vim %F", Terminal: true}},
{Filename: "nodisplay.desktop", Entry: nil},
}
func TestParse(t *testing.T) {
for _, c := range testCases {
expected := c.Entry
f, err := os.OpenFile("test/"+c.Filename, os.O_RDONLY, 0644)
if err != nil {
t.Fatal(err)
}
entry, err := Parse(f)
f.Close()
if err != nil {
t.Fatal(err)
}
if entry == nil || expected == nil {
if entry != expected {
t.Fatalf("%s: entry incorrect: got %#v, want %#v", f.Name(), entry, expected)
}
continue
}
if entry.Name != expected.Name {
t.Fatalf("%s: name incorrect: got %s, want %s", f.Name(), entry.Name, expected.Name)
}
if entry.GenericName != expected.GenericName {
t.Fatalf("%s: generic name incorrect: got %s, want %s", f.Name(), entry.GenericName, expected.GenericName)
}
if entry.Comment != expected.Comment {
t.Fatalf("%s: comment incorrect: got %s, want %s", f.Name(), entry.Comment, expected.Comment)
}
if entry.Icon != expected.Icon {
t.Fatalf("%s: icon incorrect: got %s, want %s", f.Name(), entry.Icon, expected.Icon)
}
if entry.Path != expected.Path {
t.Fatalf("%s: Path incorrect: got %s, want %s", f.Name(), entry.Path, expected.Path)
}
if entry.Exec != expected.Exec {
t.Fatalf("%s: Exec incorrect: got %s, want %s", f.Name(), entry.Exec, expected.Exec)
}
if entry.Terminal != expected.Terminal {
t.Fatalf("%s: terminal incorrect: got %v, want %v", f.Name(), entry.Terminal, expected.Terminal)
}
}
}
func BenchmarkParse(b *testing.B) {
var files []*os.File
defer func() {
for _, f := range files {
f.Close()
}
}()
for _, c := range testCases {
f, err := os.OpenFile("test/"+c.Filename, os.O_RDONLY, 0644)
if err != nil {
b.Fatal(err)
}
files = append(files, f)
}
b.StopTimer()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, f := range files {
b.StartTimer()
_, err := Parse(f)
if err != nil {
b.Fatal(err)
}
b.StopTimer()
_, err = f.Seek(0, io.SeekStart)
if err != nil {
b.Fatal(err)
}
}
}
}

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module git.sr.ht/~tslocum/desktop
go 1.12
require github.com/pkg/errors v0.8.1

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

42
run.go Normal file
View File

@ -0,0 +1,42 @@
package desktop
import (
"fmt"
"io/ioutil"
"os"
)
func RunScript(ex string) (string, error) {
runScript, err := ioutil.TempFile("", "run-*")
if err != nil {
return "", err
}
_, err = runScript.WriteString("#!/bin/sh\n")
if err != nil {
runScript.Close()
return "", err
}
_, err = runScript.WriteString(fmt.Sprintf("rm %s\n", runScript.Name()))
if err != nil {
runScript.Close()
return "", err
}
_, err = runScript.WriteString("exec " + ex + "\n")
if err != nil {
runScript.Close()
return "", err
}
err = runScript.Close()
if err != nil {
return "", err
}
err = os.Chmod(runScript.Name(), 0744)
if err != nil {
return "", err
}
return runScript.Name(), nil
}

118
scan.go Normal file
View File

@ -0,0 +1,118 @@
package desktop
import (
"os"
"path/filepath"
"runtime"
"strings"
"sync"
)
type scan struct {
e map[int][]*Entry
errs chan error
in chan *scanEntry
sync.Mutex
sync.WaitGroup
}
type scanEntry struct {
i int
f *os.File
}
func Scan(dirs []string) (map[int][]*Entry, error) {
s := &scan{e: make(map[int][]*Entry), errs: make(chan error), in: make(chan *scanEntry)}
for i := 0; i < runtime.GOMAXPROCS(-1); i++ {
go scanner(s)
}
for i, dir := range dirs {
i, dir := i, dir
s.Add(1)
go scanDir(i, dir, s)
}
done := make(chan bool, 1)
go func() {
s.Wait()
close(s.in)
done <- true
}()
select {
case err := <-s.errs:
return nil, err
case <-done:
return s.e, nil
}
}
func scanner(s *scan) {
var (
scanEntry *scanEntry
entry *Entry
err error
)
for scanEntry = range s.in {
entry, err = Parse(scanEntry.f)
scanEntry.f.Close()
if err != nil {
s.errs <- err
s.Done()
return
} else if entry == nil {
s.Done()
continue
}
s.Lock()
s.e[scanEntry.i] = append(s.e[scanEntry.i], entry)
s.Unlock()
s.Done()
}
}
func scanFunc(i int, s *scan) filepath.WalkFunc {
return func(path string, f os.FileInfo, err error) error {
s.Add(1)
go func() {
if err != nil {
s.errs <- err
s.Done()
return
}
if f == nil || f.IsDir() || !strings.HasSuffix(strings.ToLower(path), ".desktop") {
s.Done()
return
}
f, err := os.OpenFile(path, os.O_RDONLY, 0644)
if err != nil {
s.errs <- err
s.Done()
return
}
s.in <- &scanEntry{i: i, f: f}
}()
return nil
}
}
func scanDir(i int, dir string, s *scan) {
defer s.Done()
err := filepath.Walk(dir, scanFunc(i, s))
if err != nil {
s.errs <- err
}
}

29
scan_test.go Normal file
View File

@ -0,0 +1,29 @@
package desktop
import (
"testing"
)
func TestScan(t *testing.T) {
dirs := DataDirs()
_, err := Scan(dirs)
if err != nil {
t.Fatal(err)
}
}
func BenchmarkScan(b *testing.B) {
var (
dirs = DataDirs()
err error
)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err = Scan(dirs)
if err != nil {
b.Fatal(err)
}
}
}

18
test/alacritty.desktop Normal file
View File

@ -0,0 +1,18 @@
[Desktop Entry]
Type=Application
TryExec=alacritty
Path=/home/test
Exec=alacritty
Icon=Alacritty
Terminal=false
Categories=System;TerminalEmulator;
Name=Alacritty
GenericName=Terminal
Comment=A cross-platform, GPU enhanced terminal emulator
StartupWMClass=Alacritty
Actions=New;
[Desktop Action New]
Name=New Terminal
Exec=alacritty

105
test/nodisplay.desktop Normal file
View File

@ -0,0 +1,105 @@
# The vim.desktop file is generated by src/po/Makefile, do NOT edit.
# Edit the src/po/vim.desktop.in file instead.
[Desktop Entry]
# Translators: This is the Application Name used in the Vim desktop file
Name[de]=Vim
Name[eo]=Vim
Name=Vim
# Translators: This is the Generic Application Name used in the Vim desktop file
GenericName[de]=Texteditor
GenericName[eo]=Tekstoredaktilo
GenericName[ja]=テキストエディタ
GenericName=Text Editor
# Translators: This is the comment used in the Vim desktop file
Comment[de]=Textdateien bearbeiten
Comment[eo]=Redakti tekstajn dosierojn
Comment[ja]=テキストファイルを編集します
Comment=Edit text files
# The translations should come from the po file. Leave them here for now, they will
# be overwritten by the po file when generating the desktop.file.
GenericName[da]=Teksteditor
GenericName[pl]=Edytor tekstu
GenericName[is]=Ritvinnsluforrit
Comment[af]=Redigeer tekslêers
Comment[am]=የጽሑፍ ፋይሎች ያስተካክሉ
Comment[ar]=حرّر ملفات نصية
Comment[az]=Mətn fayllarını redaktə edin
Comment[be]=Рэдагаваньне тэкставых файлаў
Comment[bg]=Редактиране на текстови файлове
Comment[bn]=টেক্স্ট ফাইল এডিট করুন
Comment[bs]=Izmijeni tekstualne datoteke
Comment[ca]=Edita fitxers de text
Comment[cs]=Úprava textových souborů
Comment[cy]=Golygu ffeiliau testun
Comment[da]=Rediger tekstfiler
Comment[el]=Επεξεργασία αρχείων κειμένου
Comment[en_CA]=Edit text files
Comment[en_GB]=Edit text files
Comment[es]=Edita archivos de texto
Comment[et]=Redigeeri tekstifaile
Comment[eu]=Editatu testu-fitxategiak
Comment[fa]=ویرایش پرونده‌های متنی
Comment[fi]=Muokkaa tekstitiedostoja
Comment[fr]=Édite des fichiers texte
Comment[ga]=Eagar comhad Téacs
Comment[gu]=લખાણ ફાઇલોમાં ફેરફાર કરો
Comment[he]=ערוך קבצי טקסט
Comment[hi]=पाठ फ़ाइलें संपादित करें
Comment[hr]=Uređivanje tekstualne datoteke
Comment[hu]=Szövegfájlok szerkesztése
Comment[id]=Edit file teks
Comment[is]=Vinna með textaskrár
Comment[it]=Modifica file di testo
Comment[kn]=ಪಠ್ಯ ಕಡತಗಳನ್ನು ಸಂಪಾದಿಸು
Comment[ko]=텍스트 파일을 편집합니다
Comment[lt]=Redaguoti tekstines bylas
Comment[lv]=Rediģēt teksta failus
Comment[mk]=Уреди текстуални фајлови
Comment[ml]=വാചക രചനകള് തിരുത്തുക
Comment[mn]=Текст файл боловсруулах
Comment[mr]=गद्य फाइल संपादित करा
Comment[ms]=Edit fail teks
Comment[nb]=Rediger tekstfiler
Comment[ne]=पाठ फाइललाई संशोधन गर्नुहोस्
Comment[nl]=Tekstbestanden bewerken
Comment[nn]=Rediger tekstfiler
Comment[no]=Rediger tekstfiler
Comment[or]=ପାଠ୍ଯ ଫାଇଲଗୁଡ଼ିକୁ ସମ୍ପାଦନ କରନ୍ତୁ
Comment[pa]=ਪਾਠ ਫਾਇਲਾਂ ਸੰਪਾਦਨ
Comment[pl]=Edytuj pliki tekstowe
Comment[pt]=Editar ficheiros de texto
Comment[pt_BR]=Edite arquivos de texto
Comment[ro]=Editare fişiere text
Comment[ru]=Редактор текстовых файлов
Comment[sk]=Úprava textových súborov
Comment[sl]=Urejanje datotek z besedili
Comment[sq]=Përpuno files teksti
Comment[sr]=Измени текстуалне датотеке
Comment[sr@Latn]=Izmeni tekstualne datoteke
Comment[sv]=Redigera textfiler
Comment[ta]=உரை கோப்புகளை தொகுக்கவும்
Comment[th]=แก้ไขแฟ้มข้อความ
Comment[tk]=Metin faýllary editle
Comment[tr]=Metin dosyalarını düzenle
Comment[uk]=Редактор текстових файлів
Comment[vi]=Soạn thảo tập tin văn bản
Comment[wa]=Asspougnî des fitchîs tecses
Comment[zh_CN]=编辑文本文件
Comment[zh_TW]=編輯文字檔
TryExec=vim
Exec=vim %F
Terminal=true
Type=Application
# Translators: Search terms to find this application. Do NOT change the semicolons! The list MUST also end with a semicolon!
Keywords[de]=Text;Editor;
Keywords[eo]=Teksto;redaktilo;
Keywords[ja]=テキスト;エディタ;
Keywords=Text;editor;
# Translators: This is the Icon file name. Do NOT translate
Icon[de]=gvim
Icon[eo]=gvim
Icon=gvim
Categories=Utility;TextEditor;
StartupNotify=false
MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;
NoDisplay=true

104
test/vim.desktop Normal file
View File

@ -0,0 +1,104 @@
# The vim.desktop file is generated by src/po/Makefile, do NOT edit.
# Edit the src/po/vim.desktop.in file instead.
[Desktop Entry]
# Translators: This is the Application Name used in the Vim desktop file
Name[de]=Vim
Name[eo]=Vim
Name=Vim
# Translators: This is the Generic Application Name used in the Vim desktop file
GenericName[de]=Texteditor
GenericName[eo]=Tekstoredaktilo
GenericName[ja]=テキストエディタ
GenericName=Text Editor
# Translators: This is the comment used in the Vim desktop file
Comment[de]=Textdateien bearbeiten
Comment[eo]=Redakti tekstajn dosierojn
Comment[ja]=テキストファイルを編集します
Comment=Edit text files
# The translations should come from the po file. Leave them here for now, they will
# be overwritten by the po file when generating the desktop.file.
GenericName[da]=Teksteditor
GenericName[pl]=Edytor tekstu
GenericName[is]=Ritvinnsluforrit
Comment[af]=Redigeer tekslêers
Comment[am]=የጽሑፍ ፋይሎች ያስተካክሉ
Comment[ar]=حرّر ملفات نصية
Comment[az]=Mətn fayllarını redaktə edin
Comment[be]=Рэдагаваньне тэкставых файлаў
Comment[bg]=Редактиране на текстови файлове
Comment[bn]=টেক্স্ট ফাইল এডিট করুন
Comment[bs]=Izmijeni tekstualne datoteke
Comment[ca]=Edita fitxers de text
Comment[cs]=Úprava textových souborů
Comment[cy]=Golygu ffeiliau testun
Comment[da]=Rediger tekstfiler
Comment[el]=Επεξεργασία αρχείων κειμένου
Comment[en_CA]=Edit text files
Comment[en_GB]=Edit text files
Comment[es]=Edita archivos de texto
Comment[et]=Redigeeri tekstifaile
Comment[eu]=Editatu testu-fitxategiak
Comment[fa]=ویرایش پرونده‌های متنی
Comment[fi]=Muokkaa tekstitiedostoja
Comment[fr]=Édite des fichiers texte
Comment[ga]=Eagar comhad Téacs
Comment[gu]=લખાણ ફાઇલોમાં ફેરફાર કરો
Comment[he]=ערוך קבצי טקסט
Comment[hi]=पाठ फ़ाइलें संपादित करें
Comment[hr]=Uređivanje tekstualne datoteke
Comment[hu]=Szövegfájlok szerkesztése
Comment[id]=Edit file teks
Comment[is]=Vinna með textaskrár
Comment[it]=Modifica file di testo
Comment[kn]=ಪಠ್ಯ ಕಡತಗಳನ್ನು ಸಂಪಾದಿಸು
Comment[ko]=텍스트 파일을 편집합니다
Comment[lt]=Redaguoti tekstines bylas
Comment[lv]=Rediģēt teksta failus
Comment[mk]=Уреди текстуални фајлови
Comment[ml]=വാചക രചനകള് തിരുത്തുക
Comment[mn]=Текст файл боловсруулах
Comment[mr]=गद्य फाइल संपादित करा
Comment[ms]=Edit fail teks
Comment[nb]=Rediger tekstfiler
Comment[ne]=पाठ फाइललाई संशोधन गर्नुहोस्
Comment[nl]=Tekstbestanden bewerken
Comment[nn]=Rediger tekstfiler
Comment[no]=Rediger tekstfiler
Comment[or]=ପାଠ୍ଯ ଫାଇଲଗୁଡ଼ିକୁ ସମ୍ପାଦନ କରନ୍ତୁ
Comment[pa]=ਪਾਠ ਫਾਇਲਾਂ ਸੰਪਾਦਨ
Comment[pl]=Edytuj pliki tekstowe
Comment[pt]=Editar ficheiros de texto
Comment[pt_BR]=Edite arquivos de texto
Comment[ro]=Editare fişiere text
Comment[ru]=Редактор текстовых файлов
Comment[sk]=Úprava textových súborov
Comment[sl]=Urejanje datotek z besedili
Comment[sq]=Përpuno files teksti
Comment[sr]=Измени текстуалне датотеке
Comment[sr@Latn]=Izmeni tekstualne datoteke
Comment[sv]=Redigera textfiler
Comment[ta]=உரை கோப்புகளை தொகுக்கவும்
Comment[th]=แก้ไขแฟ้มข้อความ
Comment[tk]=Metin faýllary editle
Comment[tr]=Metin dosyalarını düzenle
Comment[uk]=Редактор текстових файлів
Comment[vi]=Soạn thảo tập tin văn bản
Comment[wa]=Asspougnî des fitchîs tecses
Comment[zh_CN]=编辑文本文件
Comment[zh_TW]=編輯文字檔
TryExec=vim
Exec=vim %F
Terminal=true
Type=Application
# Translators: Search terms to find this application. Do NOT change the semicolons! The list MUST also end with a semicolon!
Keywords[de]=Text;Editor;
Keywords[eo]=Teksto;redaktilo;
Keywords[ja]=テキスト;エディタ;
Keywords=Text;editor;
# Translators: This is the Icon file name. Do NOT translate
Icon[de]=gvim
Icon[eo]=gvim
Icon=gvim
Categories=Utility;TextEditor;
StartupNotify=false
MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;