~kota/dprint

ac23bd60ae50e13007874ececd1fb2c61abcb325 — Dakota Walsh 1 year, 19 days ago 55ed466
add popularity sorting
4 files changed, 164 insertions(+), 23 deletions(-)

M dprint.1.scd
M go.mod
M main.go
A popularity.go
M dprint.1.scd => dprint.1.scd +12 -5
@@ 6,21 6,26 @@ dprint - print specified values from desktop files to stdout.

# SYNOPSIS

_dprint_ [-v] [-d path] [-i key:val] [-o key]
_dprint_ [-v] [-p] [-d path] [-i key:val] [-o key]

# OPTIONS

*-v*
	Prints the version and exits.

*-p*
	Keep track of desktop file popularity each time an Exec or StripExec is
	selected as output. Additionally, output is sorted by popularity when using
	this option.

*-d path*
	Look for desktop files in provided path.

*-i key:val*
	Select only desktop files with a specific key:value pair.
	Filter desktop files with a specific key:value pair.

*-o key*
	Output the value associated with this specific key for each selected desktop
	Output the value associated with a specific key for each selected desktop
	file.

# DESCRIPTION


@@ 119,8 124,10 @@ True or False. List means one or more strings seperated with semi-colons.
*StartupWMClass*
	String - The string that the program will set as WM Class or WM name hint.

Additionally a special key can be used in output mode only. It will parse out
the field codes from the exec line before printing.
# SPECIAL KEYS

These special keys do not actually exist in the desktop files and can only be
used in output mode.

*StripExec*
	String - Program to execute with field codes stripped out.

M go.mod => go.mod +1 -1
@@ 1,6 1,6 @@
module git.sr.ht/~kota/dprint

go 1.12
go 1.13

require (
	git.sr.ht/~sircmpwn/getopt v0.0.0-20190621174457-292febf82fd0

M main.go => main.go +41 -17
@@ 7,6 7,7 @@ import (
	"log"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"git.sr.ht/~sircmpwn/getopt"


@@ 20,14 21,14 @@ var (
	Version string
	// Config represents the directory name under XDG_CONFIG_HOME where desktop
	// files are searched. This only matters if the -d option isn't used.
	// This value is normally set out build time and can be configured in
	// This value is normally set at build time and can be configured in
	// config.mk
	Config string
)

// usage prints some basic usage information
func usage() {
	log.Fatal("Usage: dprint [-v] [-d path] [-i key:val] [-o key]")
	log.Fatal("Usage: dprint [-v] [-p] [-d path] [-i key:val] [-o key]")
}

func main() {


@@ 36,7 37,8 @@ func main() {

	// parse arguments using getopt
	var dir, in, out string
	opts, optind, err := getopt.Getopts(os.Args, "vd:i:o:")
	var pop bool
	opts, optind, err := getopt.Getopts(os.Args, "vpd:i:o:")
	if err != nil {
		log.Print(err)
		usage()


@@ 47,6 49,8 @@ func main() {
		case 'v':
			fmt.Println("dprint " + Version)
			return
		case 'p':
			pop = true
		case 'd':
			dir = opt.Value
		case 'i':


@@ 93,11 97,20 @@ func main() {
	// filter selection by key:value pair
	entries = filter(in, entries)

	// sort by popularity
	if pop {
		sort.Sort(ByPopularity(entries))
	}

	// print output selections
	for _, entry := range entries {
		// print specified key
		if out != "" {
			fmt.Println(getOut(entry, out))
			s, err := getOut(entry, out, pop)
			if err != nil {
				log.Fatalf("failed getting output key: %v\n", err)
			}
			fmt.Println(s)
		} else {
			// print name as default
			fmt.Println(entry.Name)


@@ 225,31 238,42 @@ func checkKey(entry desktop.Entry, key string, val string) bool {
	return false
}

// getOut returns the string tied to an output value.
func getOut(entry desktop.Entry, key string) string {
// getOut returns the string tied to an output value. pop indicates to record
// the popularity count.
func getOut(entry desktop.Entry, key string, pop bool) (string, error) {
	switch key {
	case "Version":
		return entry.Version
		return entry.Version, nil
	case "Name":
		return entry.Name
		return entry.Name, nil
	case "GenericName":
		return entry.GenericName
		return entry.GenericName, nil
	case "Comment":
		return entry.Comment
		return entry.Comment, nil
	case "Icon":
		return entry.Icon
		return entry.Icon, nil
	case "URL":
		return entry.URL
		return entry.URL, nil
	case "TryExec":
		return entry.TryExec
		return entry.TryExec, nil
	case "Exec":
		return entry.Exec
		if pop {
			if err := popUp(entry); err != nil {
				return "", err
			}
		}
		return entry.Exec, nil
	case "StripExec":
		return stripExec(entry.Exec)
		if pop {
			if err := popUp(entry); err != nil {
				return "", err
			}
		}
		return stripExec(entry.Exec), nil
	case "Path":
		return entry.Path
		return entry.Path, nil
	}
	return entry.Name
	return entry.Name, nil
}

// filter selection by key:value pair.

A popularity.go => popularity.go +110 -0
@@ 0,0 1,110 @@
package main

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/rkoesters/xdg/basedir"
	"github.com/rkoesters/xdg/desktop"
)

// ByPopularity implements sort.Interface for []desktop.Entry based on popularity.
type ByPopularity []desktop.Entry

func (a ByPopularity) Len() int      { return len(a) }
func (a ByPopularity) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByPopularity) Less(i, j int) bool {
	iPop, err := getPop(a[i])
	if err != nil {
		return true
	}
	jPop, err := getPop(a[j])
	if err != nil {
		return false
	}
	return iPop > jPop
}

// popUp increments the popularity count for a desktop entry.
func popUp(entry desktop.Entry) error {
	pop, err := getPop(entry)
	if err != nil {
		return fmt.Errorf("failed reading popularity: %v", err)
	}
	// increment and write new popularity
	pop++
	err = setPop(entry, pop)
	if err != nil {
		return fmt.Errorf("failed setting popularity: %v", err)
	}
	return nil
}

// getPop returns the popularity for an entry
func getPop(entry desktop.Entry) (int, error) {
	var pop int
	// check if cache directory exists and create if missing
	d := filepath.Join(basedir.CacheHome, "dprint")
	if err := os.MkdirAll(d, 0755); err != nil {
		return pop, fmt.Errorf("failed to create cache directory: %v", err)
	}

	// read popularity of cached entry if it exists
	name := filepath.Join(d, entry.Name)
	s, err := slurp(name)
	if err != nil {
		return pop, fmt.Errorf("cache file could not be read, but exists: %v", err)
	}
	if s == "" {
		s = "0"
	}
	s = strings.TrimSuffix(s, "\n")
	pop, err = strconv.Atoi(string(s))
	if err != nil {
		return pop, fmt.Errorf("cache file is corrupt: you may need to manually edit or delete the file: %q %v", name, err)
	}
	return pop, nil
}

// set the popularity count for a desktop entry.
func setPop(entry desktop.Entry, pop int) error {
	// check if cache directory exists and create if missing
	d := filepath.Join(basedir.CacheHome, "dprint")
	if err := os.MkdirAll(d, 0755); err != nil {
		return fmt.Errorf("failed to create cache directory: %v", err)
	}
	name := filepath.Join(d, entry.Name)
	f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
	if err != nil {
		return fmt.Errorf("failed to create cache file: %v", err)
	}
	_, err = f.Write([]byte(strconv.Itoa(pop)))
	if err1 := f.Close(); err1 != nil && err == nil {
		err = err1
	}
	return err
}

// slurp a file into a string or return a blank string if the file doesn't exist.
func slurp(filename string) (string, error) {
	f, err := os.Open(filename)
	if errors.Is(err, os.ErrNotExist) {
		return "", nil
	} else if err != nil {
		return "", fmt.Errorf("failed opening file: %v", err)
	}
	b, err := io.ReadAll(f)
	if err != nil {
		return "", fmt.Errorf("failed reading file: %v", err)
	}
	err = f.Close()
	if err != nil {
		return string(b), fmt.Errorf("failed closing file: %v", err)
	}
	return string(b), nil
}