Places the seat's dmenu-compatible command, the launcher and power menu, its themes in the decided faces, and its key bindings as an i3 drop-in; Go tools menu, applications, themes and run.
197 lines
7.5 KiB
Go
197 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const nobody = 4194400
|
|
|
|
func withSession(t *testing.T) string {
|
|
t.Helper()
|
|
root := fakeMachine(t)
|
|
fakeProcess(t, nobody, "i3", "DISPLAY=:1", "XDG_SESSION_ID=2")
|
|
return root
|
|
}
|
|
|
|
func TestTheMenuAnswersTheChosenLineAndItsIndex(t *testing.T) {
|
|
withSession(t)
|
|
bin := fakeBinaries(t, map[string]string{"rofi": `echo "$* DISPLAY=$DISPLAY" > "$LOG"; cat > "$LOG.in"; echo "second"`})
|
|
t.Setenv("LOG", filepath.Join(bin, "log"))
|
|
q, err := menuQuestion(map[string]any{"items": []any{"first", "second"}, "prompt": "Pick", "message": "why"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := Menu(q)
|
|
if err != nil || got.Chosen != "second" || got.Index != 1 || got.Cancelled || got.Custom {
|
|
t.Fatalf("%+v, %v", got, err)
|
|
}
|
|
asked, _ := os.ReadFile(filepath.Join(bin, "log"))
|
|
if strings.TrimSpace(string(asked)) != "-dmenu -i -p Pick -format s -mesg why -no-custom DISPLAY=:1" {
|
|
t.Fatalf("rofi was asked: %s", asked)
|
|
}
|
|
in, _ := os.ReadFile(filepath.Join(bin, "log.in"))
|
|
if string(in) != "first\nsecond\n" {
|
|
t.Fatalf("the choices, one per line: %q", in)
|
|
}
|
|
}
|
|
|
|
func TestClosingTheMenuOrNotAnsweringIsACancelNotAnError(t *testing.T) {
|
|
withSession(t)
|
|
fakeBinaries(t, map[string]string{"rofi": `exit 1`})
|
|
got, err := Menu(MenuQuestion{Items: []string{"a"}, Prompt: "p", Timeout: 5e9})
|
|
if err != nil || !got.Cancelled || got.Index != -1 {
|
|
t.Fatalf("closed: %+v, %v", got, err)
|
|
}
|
|
fakeBinaries(t, map[string]string{"rofi": `sleep 30`})
|
|
got, err = Menu(MenuQuestion{Items: []string{"a"}, Prompt: "p", Timeout: 2e8})
|
|
if err != nil || !got.Cancelled || !got.TimedOut {
|
|
t.Fatalf("not answered: %+v, %v", got, err)
|
|
}
|
|
fakeBinaries(t, map[string]string{"rofi": `echo typed`})
|
|
got, err = Menu(MenuQuestion{Items: []string{"a"}, Prompt: "p", AllowCustom: true, Timeout: 5e9})
|
|
if err != nil || got.Chosen != "typed" || !got.Custom || got.Index != -1 {
|
|
t.Fatalf("typed: %+v, %v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestAMenuQuestionIsBoundedAndOneLinePerChoice(t *testing.T) {
|
|
for _, bad := range []map[string]any{
|
|
{},
|
|
{"items": []any{}},
|
|
{"items": []any{"two\nlines"}},
|
|
{"items": []any{"a"}, "timeout_seconds": float64(60)},
|
|
{"items": []any{3.0}},
|
|
} {
|
|
if _, err := menuQuestion(bad); err == nil {
|
|
t.Errorf("accepted %v", bad)
|
|
}
|
|
}
|
|
q, err := menuQuestion(map[string]any{"items": []any{"a"}})
|
|
if err != nil || q.Prompt != "Choose" || q.Timeout.Seconds() != menuDefault {
|
|
t.Fatalf("defaults: %+v, %v", q, err)
|
|
}
|
|
}
|
|
|
|
func TestWithoutASessionTheMenuSaysSo(t *testing.T) {
|
|
fakeMachine(t)
|
|
if _, err := Menu(MenuQuestion{Items: []string{"a"}, Timeout: 1e9}); !errors.Is(err, ErrNoSession) {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func writeFile(t *testing.T, path, content string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestApplicationsAreTheDesktopEntriesTheAccountsWinning(t *testing.T) {
|
|
root := t.TempDir()
|
|
own, system := filepath.Join(root, "own"), filepath.Join(root, "system")
|
|
writeFile(t, filepath.Join(system, "firefox.desktop"), "[Desktop Entry]\nType=Application\nName=Firefox\nExec=firefox %u\nCategories=Network;WebBrowser;\n[Desktop Action new]\nName=New Window\nExec=firefox --new-window\n")
|
|
writeFile(t, filepath.Join(own, "firefox.desktop"), "[Desktop Entry]\nType=Application\nName=Firefox (mine)\nExec=firefox -P work %u\n")
|
|
writeFile(t, filepath.Join(system, "kde", "konsole.desktop"), "[Desktop Entry]\nType=Application\nName=Konsole\nExec=konsole\nTerminal=false\nKeywords=terminal;shell;\n")
|
|
writeFile(t, filepath.Join(system, "hidden.desktop"), "[Desktop Entry]\nType=Application\nName=Helper\nExec=helper\nNoDisplay=true\n")
|
|
writeFile(t, filepath.Join(system, "link.desktop"), "[Desktop Entry]\nType=Link\nName=A link\nURL=https://example.org\n")
|
|
got := Applications([]string{own, system}, "", false, 10)
|
|
var names []string
|
|
for _, a := range got.Applications {
|
|
names = append(names, a.ID+"="+a.Name)
|
|
}
|
|
if !reflect.DeepEqual(names, []string{"firefox.desktop=Firefox (mine)", "kde-konsole.desktop=Konsole"}) {
|
|
t.Fatalf("%v", names)
|
|
}
|
|
if got := Applications([]string{own, system}, "SHELL", false, 10); got.Count != 1 || got.Applications[0].Name != "Konsole" {
|
|
t.Fatalf("by keyword: %+v", got)
|
|
}
|
|
if got := Applications([]string{own, system}, "", true, 1); got.Count != 3 || !got.Truncated || len(got.Applications) != 1 {
|
|
t.Fatalf("hidden and limited: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestThemesAreListedOnceWithTheConfiguredOne(t *testing.T) {
|
|
root := t.TempDir()
|
|
mine, system := filepath.Join(root, "mine"), filepath.Join(root, "system")
|
|
writeFile(t, filepath.Join(mine, "mesh.rasi"), "*{}")
|
|
writeFile(t, filepath.Join(system, "mesh.rasi"), "*{}")
|
|
writeFile(t, filepath.Join(system, "Arc.rasi"), "*{}")
|
|
conf, _ := os.ReadFile(filepath.Join("..", "..", "files", "config.rasi"))
|
|
writeFile(t, filepath.Join(root, "config.rasi"), string(conf))
|
|
got := Themes([]themeDir{{mine, "mine"}, {system, "system"}}, filepath.Join(root, "config.rasi"))
|
|
if got.Current != "mesh" || len(got.Themes) != 2 || got.Themes[1].Source != "mine" {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
}
|
|
|
|
func TestExecIsSplitAsTheDesktopEntrySpecQuotesIt(t *testing.T) {
|
|
for in, want := range map[string][]string{
|
|
"firefox %u": {"firefox"},
|
|
`sh -c "echo \"a b\"" %F`: {"sh", "-c", `echo "a b"`},
|
|
"printf 100%% --flag": {"printf", "100%", "--flag"},
|
|
`"/opt/My App/run" --x`: {"/opt/My App/run", "--x"},
|
|
} {
|
|
got, err := execWords(in)
|
|
if err != nil || !reflect.DeepEqual(got, want) {
|
|
t.Errorf("%s: %q, %v", in, got, err)
|
|
}
|
|
}
|
|
for _, bad := range []string{`"open`, "%u"} {
|
|
if _, err := execWords(bad); err == nil {
|
|
t.Errorf("%s was accepted", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunStartsAnApplicationUnderTheAccountsServiceManager(t *testing.T) {
|
|
root := withSession(t)
|
|
writeFile(t, filepath.Join(root, "home", ".local", "share", "applications", "htop.desktop"),
|
|
"[Desktop Entry]\nType=Application\nName=htop\nExec=htop\nTerminal=true\n")
|
|
t.Setenv("XDG_DATA_DIRS", filepath.Join(root, "none"))
|
|
// The account's runtime directory, through which its service manager is reached.
|
|
if err := os.MkdirAll(filepath.Join(runUserDir, strconv.Itoa(os.Getuid())), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bin := fakeBinaries(t, map[string]string{"systemd-run": `echo "$*" > "$LOG"`, "systemctl": `true`})
|
|
t.Setenv("LOG", filepath.Join(bin, "log"))
|
|
got, err := Run("htop.desktop", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(got.Argv, []string{"rofi-sensible-terminal", "-e", "htop"}) || !strings.HasPrefix(got.Unit, "rofi-run-") {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
asked, _ := os.ReadFile(filepath.Join(bin, "log"))
|
|
if !strings.Contains(string(asked), "--setenv=DISPLAY=:1 --setenv=XDG_SESSION_ID=2 -- rofi-sensible-terminal -e htop") {
|
|
t.Fatalf("systemd-run was asked: %s", asked)
|
|
}
|
|
if _, err := Run("", nil); err == nil {
|
|
t.Fatal("nothing to run was accepted")
|
|
}
|
|
if _, err := Run("nope.desktop", nil); err == nil {
|
|
t.Fatal("a missing entry was accepted")
|
|
}
|
|
}
|
|
|
|
func TestTheDmenuCommandHandsRofiWhatItUnderstands(t *testing.T) {
|
|
fakeBinaries(t, map[string]string{"rofi": `for a in "$@"; do printf '[%s]' "$a"; done`})
|
|
shim, _ := filepath.Abs(filepath.Join("..", "..", "files", "bin", "dmenu"))
|
|
out, err := exec.Command(shim, "-b", "-fn", "Mono 10", "-l", "8", "-p", "pick one", "-nb", "#000", "-i").Output()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(out) != "[-dmenu][-l][8][-p][pick one][-i]" {
|
|
t.Fatalf("rofi was handed %s", out)
|
|
}
|
|
}
|