openrazer (the official driver, daemon and library), polychromatic (the AUR tray, kept as found; its i3 line moves out of the i3 module into its own node-display-session contribution), forticlient (the AUR VPN client: its service declared, its configuration never read) and nm-applet (the desktop half of NetworkManager, apart from the server-side module). The openrazer daemon fails on both workstations because the account is not in the openrazer group; openrazer_check names it and the README carries the one-off step, since the account's user resource is zsh's. desktop.go learns to tell a program from another sharing its 15-character command name, so polychromatic's tools never count or end themselves, and a copies test holds the six carriers to one text.
214 lines
7.7 KiB
Go
214 lines
7.7 KiB
Go
package main
|
|
|
|
// Polychromatic's tray as the tools see it: its processes, its one start (the module's line in the
|
|
// window manager's configuration), the package's login helper and the one setting of the application's
|
|
// that makes the helper start the tray too, and whether the daemon it drives answers. The daemon is
|
|
// the openrazer module's; it is asked with --auto-start=no, so a question never starts it.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
trayComm = "polychromatic-t" // the kernel keeps 15 characters of polychromatic-tray-applet
|
|
trayWord = "polychromatic-tray-applet"
|
|
trayBin = "/usr/bin/polychromatic-tray-applet"
|
|
helperEntry = "polychromatic-autostart.desktop"
|
|
restartAs = "mesh-polychromatic-tray"
|
|
packageFor = "polychromatic"
|
|
prefsFile = ".config/polychromatic/preferences.json"
|
|
settingName = "Preferences → Tray → \"Start the tray applet when I log on\""
|
|
)
|
|
|
|
// trays are the tray's processes. Its cut command name is this bundle's own too, so the program is
|
|
// matched by its full name.
|
|
func (m *Machine) trays() []Proc { return m.procsOf(trayComm, trayWord) }
|
|
|
|
// TraySetting is the application's own setting that makes its login helper start the tray. The
|
|
// module never writes it: the application rewrites its preferences file itself.
|
|
type TraySetting struct {
|
|
// Autostart is the setting; Polychromatic's default is on.
|
|
Autostart bool `json:"autostart"`
|
|
Delay int `json:"autostart_delay_seconds"`
|
|
From string `json:"from"`
|
|
}
|
|
|
|
// setting reads the tray keys of the preferences file and nothing else of it.
|
|
func (m *Machine) setting() TraySetting {
|
|
s := TraySetting{Autostart: true, From: "Polychromatic's default (no preferences file)"}
|
|
raw, err := readBounded(m.home(prefsFile))
|
|
if err != nil {
|
|
return s
|
|
}
|
|
var doc struct {
|
|
Tray struct {
|
|
Autostart *bool `json:"autostart"`
|
|
Delay int `json:"autostart_delay"`
|
|
} `json:"tray"`
|
|
}
|
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
|
s.From = "Polychromatic's default (the preferences file does not parse)"
|
|
return s
|
|
}
|
|
s.From = "~/" + prefsFile
|
|
if doc.Tray.Autostart != nil {
|
|
s.Autostart = *doc.Tray.Autostart
|
|
}
|
|
s.Delay = doc.Tray.Delay
|
|
return s
|
|
}
|
|
|
|
// Backend is whether the openrazer daemon answers, and how many devices it has.
|
|
type Backend struct {
|
|
Answers bool `json:"answers"`
|
|
Devices int `json:"devices"`
|
|
Unanswered string `json:"unanswered,omitempty"`
|
|
}
|
|
|
|
func (m *Machine) backend() Backend {
|
|
args := []string{"--user", "--auto-start=no", "--json=short", "call", "org.razer", "/org/razer", "razer.devices", "getDevices"}
|
|
o := m.cmd(5*time.Second, m.bus(), "busctl", args...)
|
|
if err := failed(o, "busctl", args...); err != nil {
|
|
return Backend{Unanswered: "the openrazer daemon does not answer (asked without starting it): " + err.Error()}
|
|
}
|
|
var doc struct {
|
|
Data [][]string `json:"data"`
|
|
}
|
|
if err := json.Unmarshal([]byte(o.Stdout), &doc); err != nil || len(doc.Data) != 1 {
|
|
return Backend{Unanswered: "the daemon's device list is not busctl's JSON"}
|
|
}
|
|
return Backend{Answers: true, Devices: len(doc.Data[0])}
|
|
}
|
|
|
|
// foreign says whether the package came from outside the official repositories (pacman -Qm).
|
|
func (m *Machine) foreign() bool {
|
|
o := m.cmd(0, nil, "pacman", "-Qqm", packageFor)
|
|
return o.Err == nil && o.Code == 0 && strings.TrimSpace(o.Stdout) == packageFor
|
|
}
|
|
|
|
// StatusAnswer is what polychromatic_status answers.
|
|
type StatusAnswer struct {
|
|
Installed string `json:"installed,omitempty"`
|
|
Foreign bool `json:"outside_official_repositories"`
|
|
Tray []Proc `json:"tray"`
|
|
StartedBy []string `json:"started_by"`
|
|
Helper Autostart `json:"login_helper"`
|
|
Setting TraySetting `json:"tray_setting"`
|
|
Backend Backend `json:"backend"`
|
|
}
|
|
|
|
// Status reads the tray, what starts it, and whether the daemon behind it answers.
|
|
func (m *Machine) Status() (StatusAnswer, error) {
|
|
s := StatusAnswer{Tray: m.trays(), StartedBy: []string{}, Helper: m.autostart(helperEntry),
|
|
Setting: m.setting(), Backend: m.backend()}
|
|
if s.Tray == nil {
|
|
s.Tray = []Proc{}
|
|
}
|
|
v, err := m.installed(packageFor)
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
s.Installed = v
|
|
if v != "" {
|
|
s.Foreign = m.foreign()
|
|
}
|
|
for _, l := range m.i3Starts(trayWord) {
|
|
s.StartedBy = append(s.StartedBy, "window manager: "+l)
|
|
}
|
|
if s.Helper.Starts && s.Setting.Autostart {
|
|
s.StartedBy = append(s.StartedBy, "the login helper ("+s.Helper.From+"), as the tray setting is on")
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// RestartAnswer is what polychromatic_restart answers.
|
|
type RestartAnswer struct {
|
|
Ended []int `json:"ended"`
|
|
Killed []int `json:"killed,omitempty"`
|
|
Running []Proc `json:"running"`
|
|
Session Session `json:"session"`
|
|
Unit string `json:"unit"`
|
|
}
|
|
|
|
// Restart ends the tray and starts it again in the operator's session, under the account's service
|
|
// manager.
|
|
func (m *Machine) Restart() (RestartAnswer, error) {
|
|
s, err := m.session()
|
|
if err != nil {
|
|
return RestartAnswer{}, err
|
|
}
|
|
a := RestartAnswer{Session: s, Unit: restartAs + ".service"}
|
|
a.Ended, a.Killed = m.stopProcs(5*time.Second, m.trays())
|
|
if err := m.detach(s, restartAs, trayBin); err != nil {
|
|
return a, err
|
|
}
|
|
a.Running = m.waitForOf(trayComm, trayWord, 4*time.Second)
|
|
if len(a.Running) == 0 {
|
|
return a, fmt.Errorf("the tray was started as %s but no %s process appeared within 4 s: "+
|
|
"see `journalctl --user -u %s`", a.Unit, trayWord, a.Unit)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// CheckAnswer is what polychromatic_check answers.
|
|
type CheckAnswer struct {
|
|
OK bool `json:"ok"`
|
|
Findings []Finding `json:"findings"`
|
|
Starts []string `json:"starts"`
|
|
}
|
|
|
|
// Check verifies what the module promises and relies on: the package (found, not installed by the
|
|
// mesh); one start, the module's window-manager line; the tray running once; and the daemon answering.
|
|
func (m *Machine) Check() (CheckAnswer, error) {
|
|
a := CheckAnswer{Findings: []Finding{}, Starts: []string{}}
|
|
add := func(what, do string) { a.Findings = append(a.Findings, Finding{what, do}) }
|
|
v, err := m.installed(packageFor)
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
if v == "" {
|
|
add("polychromatic is not installed. It is outside the official repositories, so the mesh does not install it",
|
|
"install it from the AUR by hand; it brings the openrazer packages with it")
|
|
}
|
|
|
|
lines := m.i3Starts(trayWord)
|
|
for _, l := range lines {
|
|
a.Starts = append(a.Starts, "window manager: "+l)
|
|
}
|
|
switch {
|
|
case len(lines) == 0:
|
|
add("the window manager does not start the tray", "push the module: its line in the window manager's configuration is the tray's start")
|
|
case len(lines) > 1:
|
|
for _, l := range lines[1:] {
|
|
add("a second start: "+l, "remove the line; the module's line is the tray's one start")
|
|
}
|
|
}
|
|
helper := m.autostart(helperEntry)
|
|
if set := m.setting(); helper.Starts && set.Autostart {
|
|
a.Starts = append(a.Starts, "login helper: "+helper.From+" (the tray setting is on)")
|
|
add("a second start: the package's login helper starts the tray too, as the tray setting is on ("+set.From+")",
|
|
"in Polychromatic, untick "+settingName+". The helper keeps its other work: it resumes the devices' effects")
|
|
}
|
|
if own := m.autostart(trayWord + ".desktop"); own.Starts {
|
|
a.Starts = append(a.Starts, "XDG autostart: "+own.From)
|
|
add("a second start: "+own.From, "remove it; the module's line is the tray's one start")
|
|
}
|
|
|
|
if _, err := m.session(); err == nil {
|
|
switch running := m.trays(); {
|
|
case len(running) == 0:
|
|
add("no tray runs in the desktop session", "polychromatic_restart")
|
|
case len(running) > 1:
|
|
add(fmt.Sprintf("%d trays run", len(running)), "polychromatic_restart ends them all and starts one")
|
|
}
|
|
}
|
|
if b := m.backend(); !b.Answers {
|
|
add(b.Unanswered+": the tray has no devices to show", "assign the openrazer module; openrazer_check says why the daemon is not running")
|
|
}
|
|
a.OK = len(a.Findings) == 0
|
|
return a, nil
|
|
}
|