client/systray: add installer for a freedesktop autostart file (#18767)

Adds freedesktop as an option for installing autostart desktop files for
starting the systray application.

Fixes #18766

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
This commit is contained in:
Claus Lensbøl
2026-02-26 12:59:45 -05:00
committed by GitHub
parent da90ea664d
commit 5ac35b665b
8 changed files with 358 additions and 2 deletions
+43
View File
@@ -0,0 +1,43 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package freedesktop provides helpers for freedesktop systems.
package freedesktop
import "strings"
const needsEscape = " \t\n\"'\\><~|&;$*?#()`"
var escaper = strings.NewReplacer(`"`, `\"`, "`", "\\`", `$`, `\$`, `\`, `\\`)
// Quote quotes according to the Desktop Entry Specification, as below:
//
// Arguments may be quoted in whole. If an argument contains a reserved
// character the argument must be quoted. The rules for quoting of arguments is
// also applicable to the executable name or path of the executable program as
// provided.
//
// Quoting must be done by enclosing the argument between double quotes and
// escaping the double quote character, backtick character ("`"), dollar sign
// ("$") and backslash character ("\") by preceding it with an additional
// backslash character. Implementations must undo quoting before expanding field
// codes and before passing the argument to the executable program. Reserved
// characters are space (" "), tab, newline, double quote, single quote ("'"),
// backslash character ("\"), greater-than sign (">"), less-than sign ("<"),
// tilde ("~"), vertical bar ("|"), ampersand ("&"), semicolon (";"), dollar
// sign ("$"), asterisk ("*"), question mark ("?"), hash mark ("#"), parenthesis
// ("(") and (")") and backtick character ("`").
func Quote(s string) string {
if s == "" {
return `""`
}
if !strings.ContainsAny(s, needsEscape) {
return s
}
var b strings.Builder
b.WriteString(`"`)
escaper.WriteString(&b, s)
b.WriteString(`"`)
return b.String()
}
+145
View File
@@ -0,0 +1,145 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package freedesktop
import (
"strings"
"testing"
)
func TestEscape(t *testing.T) {
tests := []struct {
name, input, want string
}{
{
name: "no illegal chars",
input: "/home/user",
want: "/home/user",
},
{
name: "empty string",
input: "",
want: "\"\"",
},
{
name: "space",
input: " ",
want: "\" \"",
},
{
name: "tab",
input: "\t",
want: "\"\t\"",
},
{
name: "newline",
input: "\n",
want: "\"\n\"",
},
{
name: "double quote",
input: "\"",
want: "\"\\\"\"",
},
{
name: "single quote",
input: "'",
want: "\"'\"",
},
{
name: "backslash",
input: "\\",
want: "\"\\\\\"",
},
{
name: "greater than",
input: ">",
want: "\">\"",
},
{
name: "less than",
input: "<",
want: "\"<\"",
},
{
name: "tilde",
input: "~",
want: "\"~\"",
},
{
name: "pipe",
input: "|",
want: "\"|\"",
},
{
name: "ampersand",
input: "&",
want: "\"&\"",
},
{
name: "semicolon",
input: ";",
want: "\";\"",
},
{
name: "dollar",
input: "$",
want: "\"\\$\"",
},
{
name: "asterisk",
input: "*",
want: "\"*\"",
},
{
name: "question mark",
input: "?",
want: "\"?\"",
},
{
name: "hash",
input: "#",
want: "\"#\"",
},
{
name: "open paren",
input: "(",
want: "\"(\"",
},
{
name: "close paren",
input: ")",
want: "\")\"",
},
{
name: "backtick",
input: "`",
want: "\"\\`\"",
},
{
name: "char without escape",
input: "/home/user\t",
want: "\"/home/user\t\"",
},
{
name: "char with escape",
input: "/home/user\\",
want: "\"/home/user\\\\\"",
},
{
name: "all illegal chars",
input: "/home/user" + needsEscape,
want: "\"/home/user \t\n\\\"'\\\\><~|&;\\$*?#()\\`\"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Quote(tt.input)
if strings.Compare(got, tt.want) != 0 {
t.Errorf("expected %s, got %s", tt.want, got)
}
})
}
}