...

Source file src/code.rocketnine.space/tslocum/desktop/entry.go

Documentation: code.rocketnine.space/tslocum/desktop

     1  package desktop
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"strconv"
    10  	"strings"
    11  )
    12  
    13  // EntryType may be Application, Link or Directory.
    14  type EntryType int
    15  
    16  // All entry types
    17  const (
    18  	Unknown     EntryType = iota // Unspecified or unrecognized
    19  	Application                  // Execute command
    20  	Link                         // Open browser
    21  	Directory                    // Open file manager
    22  )
    23  
    24  const sectionHeaderNotFoundError = "section header not found"
    25  
    26  func (t EntryType) String() string {
    27  	switch t {
    28  	case Unknown:
    29  		return "Unknown"
    30  	case Application:
    31  		return "Application"
    32  	case Link:
    33  		return "Link"
    34  	case Directory:
    35  		return "Directory"
    36  	}
    37  
    38  	return strconv.Itoa(int(t))
    39  }
    40  
    41  var (
    42  	entryHeader      = []byte("[desktop entry]")
    43  	entryType        = []byte("type=")
    44  	entryName        = []byte("name=")
    45  	entryGenericName = []byte("genericname=")
    46  	entryComment     = []byte("comment=")
    47  	entryIcon        = []byte("icon=")
    48  	entryPath        = []byte("path=")
    49  	entryExec        = []byte("exec=")
    50  	entryURL         = []byte("url=")
    51  	entryTerminal    = []byte("terminal=true")
    52  	entryNoDisplay   = []byte("nodisplay=true")
    53  	entryHidden      = []byte("hidden=true")
    54  )
    55  
    56  var quotes = map[string]string{
    57  	`%%`:         `%`,
    58  	`\\\\ `:      `\\ `,
    59  	`\\\\` + "`": `\\` + "`",
    60  	`\\\\$`:      `\\$`,
    61  	`\\\\(`:      `\\(`,
    62  	`\\\\)`:      `\\)`,
    63  	`\\\\\`:      `\\\`,
    64  	`\\\\\\\\`:   `\\\\`,
    65  }
    66  
    67  // Entry represents a parsed desktop entry.
    68  type Entry struct {
    69  	// Type is the type of the entry. It may be Application, Link or Directory.
    70  	Type EntryType
    71  
    72  	// Name is the name of the entry.
    73  	Name string
    74  
    75  	// GenericName is a generic description of the entry.
    76  	GenericName string
    77  
    78  	// Comment is extra information about the entry.
    79  	Comment string
    80  
    81  	// Icon is the path to an icon file or name of a themed icon.
    82  	Icon string
    83  
    84  	// Path is the directory to start in.
    85  	Path string
    86  
    87  	// Exec is the command(s) to be executed when launched.
    88  	Exec string
    89  
    90  	// URL is the URL to be visited when launched.
    91  	URL string
    92  
    93  	// Terminal controls whether to run in a terminal.
    94  	Terminal bool
    95  }
    96  
    97  // ExpandExec fills keywords in the provided entry's Exec with user arguments.
    98  func (e *Entry) ExpandExec(args string) string {
    99  	ex := e.Exec
   100  
   101  	ex = strings.ReplaceAll(ex, "%F", args)
   102  	ex = strings.ReplaceAll(ex, "%f", args)
   103  	ex = strings.ReplaceAll(ex, "%U", args)
   104  	ex = strings.ReplaceAll(ex, "%u", args)
   105  
   106  	return ex
   107  }
   108  
   109  func unquoteExec(ex string) string {
   110  	for qs, qr := range quotes {
   111  		ex = strings.ReplaceAll(ex, qs, qr)
   112  	}
   113  
   114  	return ex
   115  }
   116  
   117  // Parse reads and parses a .desktop file into an *Entry.
   118  func Parse(content io.Reader, buf []byte) (*Entry, error) {
   119  	var (
   120  		scanner         = bufio.NewScanner(content)
   121  		scannedBytes    []byte
   122  		scannedBytesLen int
   123  
   124  		entry       Entry
   125  		foundHeader bool
   126  	)
   127  
   128  	scanner.Buffer(buf, len(buf))
   129  	for scanner.Scan() {
   130  		scannedBytes = bytes.TrimSpace(scanner.Bytes())
   131  		scannedBytesLen = len(scannedBytes)
   132  
   133  		if scannedBytesLen == 0 || scannedBytes[0] == byte('#') {
   134  			continue
   135  		} else if scannedBytes[0] == byte('[') {
   136  			if !foundHeader {
   137  				if scannedBytesLen < 15 || !bytes.EqualFold(scannedBytes[0:15], entryHeader) {
   138  					return nil, errors.New(sectionHeaderNotFoundError)
   139  				}
   140  
   141  				foundHeader = true
   142  			} else {
   143  				break // Start of new section
   144  			}
   145  		} else if scannedBytesLen >= 6 && bytes.EqualFold(scannedBytes[0:5], entryType) {
   146  			t := strings.ToLower(string(scannedBytes[5:]))
   147  			switch t {
   148  			case "application":
   149  				entry.Type = Application
   150  			case "link":
   151  				entry.Type = Link
   152  			case "directory":
   153  				entry.Type = Directory
   154  			}
   155  		} else if scannedBytesLen >= 6 && bytes.EqualFold(scannedBytes[0:5], entryName) {
   156  			entry.Name = string(scannedBytes[5:])
   157  		} else if scannedBytesLen >= 13 && bytes.EqualFold(scannedBytes[0:12], entryGenericName) {
   158  			entry.GenericName = string(scannedBytes[12:])
   159  		} else if scannedBytesLen >= 9 && bytes.EqualFold(scannedBytes[0:8], entryComment) {
   160  			entry.Comment = string(scannedBytes[8:])
   161  		} else if scannedBytesLen >= 6 && bytes.EqualFold(scannedBytes[0:5], entryIcon) {
   162  			entry.Icon = string(scannedBytes[5:])
   163  		} else if scannedBytesLen >= 6 && bytes.EqualFold(scannedBytes[0:5], entryPath) {
   164  			entry.Path = string(scannedBytes[5:])
   165  		} else if scannedBytesLen >= 6 && bytes.EqualFold(scannedBytes[0:5], entryExec) {
   166  			entry.Exec = unquoteExec(string(scannedBytes[5:]))
   167  		} else if scannedBytesLen >= 5 && bytes.EqualFold(scannedBytes[0:4], entryURL) {
   168  			entry.URL = string(scannedBytes[4:])
   169  		} else if scannedBytesLen == 13 && bytes.EqualFold(scannedBytes, entryTerminal) {
   170  			entry.Terminal = true
   171  		} else if (scannedBytesLen == 14 && bytes.EqualFold(scannedBytes, entryNoDisplay)) || (scannedBytesLen == 11 && bytes.EqualFold(scannedBytes, entryHidden)) {
   172  			return nil, nil
   173  		}
   174  	}
   175  
   176  	err := scanner.Err()
   177  	if err == nil && !foundHeader {
   178  		err = errors.New(sectionHeaderNotFoundError)
   179  	}
   180  	if err != nil {
   181  		return nil, fmt.Errorf("failed to parse desktop entry: %s", err)
   182  	}
   183  
   184  	return &entry, nil
   185  }
   186  

View as plain text