feh: the wallpaper as a module, its image an archive of its own (hq ADR 0208, ADR 0205)
~/.fehbg stops pointing into the predecessor's tree; the session's xinitrc slot runs it once; Go tools set (for the session) and current.
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// modes are feh's background modes, by the word feh_set takes.
|
||||
var modes = []string{"fill", "center", "max", "scale", "tile"}
|
||||
|
||||
// Wallpaper is images and how they are laid on the screens.
|
||||
type Wallpaper struct {
|
||||
Images []string `json:"images"`
|
||||
Mode string `json:"mode"`
|
||||
At string `json:"at,omitempty"`
|
||||
}
|
||||
|
||||
func fehbg() string { return filepath.Join(operatorHome(), ".fehbg") }
|
||||
|
||||
// sessionRecord is where feh_set notes what it put up, for feh_current: in the runtime directory,
|
||||
// so it lasts exactly as long as the login, like the wallpaper itself.
|
||||
func sessionRecord(s Session) string {
|
||||
if s.RuntimeDir == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(s.RuntimeDir, "feh", "current.json")
|
||||
}
|
||||
|
||||
// SetResult is what feh_set answers.
|
||||
type SetResult struct {
|
||||
Wallpaper
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
// Set puts images up as the wallpaper for this session.
|
||||
func Set(images []string, mode string) (SetResult, error) {
|
||||
if mode == "" {
|
||||
mode = "fill"
|
||||
}
|
||||
known := false
|
||||
for _, m := range modes {
|
||||
known = known || m == mode
|
||||
}
|
||||
if !known {
|
||||
return SetResult{}, fmt.Errorf("mode %q is one of %s", mode, strings.Join(modes, ", "))
|
||||
}
|
||||
if len(images) == 0 {
|
||||
return SetResult{}, errors.New("images is required: at least one image")
|
||||
}
|
||||
var paths []string
|
||||
for _, img := range images {
|
||||
p := img
|
||||
if strings.HasPrefix(p, "~/") {
|
||||
p = filepath.Join(operatorHome(), p[2:])
|
||||
}
|
||||
if !filepath.IsAbs(p) {
|
||||
p = filepath.Join(operatorHome(), p)
|
||||
}
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
return SetResult{}, fmt.Errorf("image %s: %w", img, err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return SetResult{}, fmt.Errorf("image %s is a directory", img)
|
||||
}
|
||||
paths = append(paths, p)
|
||||
}
|
||||
s, err := findSession()
|
||||
if err != nil {
|
||||
return SetResult{}, err
|
||||
}
|
||||
args := append([]string{"--no-fehbg", "--bg-" + mode}, paths...)
|
||||
r, err := s.run(15*time.Second, "", "feh", args...)
|
||||
if err != nil {
|
||||
return SetResult{}, err
|
||||
}
|
||||
if r.Code != 0 {
|
||||
return SetResult{}, fmt.Errorf("feh: %s", strings.TrimSpace(r.Stderr))
|
||||
}
|
||||
w := Wallpaper{Images: paths, Mode: mode, At: time.Now().Format(time.RFC3339)}
|
||||
if rec := sessionRecord(s); rec != "" {
|
||||
if err := os.MkdirAll(filepath.Dir(rec), 0o700); err == nil {
|
||||
raw, _ := json.Marshal(w)
|
||||
_ = os.WriteFile(rec, raw, 0o600)
|
||||
}
|
||||
}
|
||||
return SetResult{Wallpaper: w, Note: "for this session; the declared wallpaper returns at the next login"}, nil
|
||||
}
|
||||
|
||||
// CurrentResult is what feh_current answers.
|
||||
type CurrentResult struct {
|
||||
Declared *Wallpaper `json:"declared"`
|
||||
Session *Wallpaper `json:"session,omitempty"`
|
||||
}
|
||||
|
||||
var bgMode = regexp.MustCompile(`--bg-(fill|center|max|scale|tile)\b`)
|
||||
|
||||
// Current is the declared wallpaper and the one set in this session.
|
||||
func Current() (CurrentResult, error) {
|
||||
var out CurrentResult
|
||||
if raw, err := os.ReadFile(fehbg()); err == nil {
|
||||
out.Declared = parseFehbg(string(raw), operatorHome())
|
||||
}
|
||||
if rec := sessionRecord(findEnvironment()); rec != "" {
|
||||
if raw, err := os.ReadFile(rec); err == nil {
|
||||
var w Wallpaper
|
||||
if json.Unmarshal(raw, &w) == nil {
|
||||
out.Session = &w
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// parseFehbg reads the feh line of a ~/.fehbg: its mode and its images, with $HOME expanded.
|
||||
func parseFehbg(script, home string) *Wallpaper {
|
||||
for _, line := range strings.Split(script, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, "feh ") {
|
||||
continue
|
||||
}
|
||||
w := &Wallpaper{Images: []string{}}
|
||||
if m := bgMode.FindStringSubmatch(line); m != nil {
|
||||
w.Mode = m[1]
|
||||
}
|
||||
for _, word := range shellWords(line)[1:] {
|
||||
if strings.HasPrefix(word, "-") {
|
||||
continue
|
||||
}
|
||||
word = strings.ReplaceAll(strings.ReplaceAll(word, "${HOME}", home), "$HOME", home)
|
||||
w.Images = append(w.Images, word)
|
||||
}
|
||||
return w
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// shellWords splits a simple command line on blanks, honouring single and double quotes.
|
||||
func shellWords(line string) []string {
|
||||
var words []string
|
||||
var cur strings.Builder
|
||||
var quote byte
|
||||
in := false
|
||||
for i := 0; i < len(line); i++ {
|
||||
c := line[i]
|
||||
switch {
|
||||
case quote != 0 && c == quote:
|
||||
quote = 0
|
||||
case quote != 0:
|
||||
cur.WriteByte(c)
|
||||
case c == '\'' || c == '"':
|
||||
quote, in = c, true
|
||||
case c == ' ' || c == '\t':
|
||||
if in {
|
||||
words = append(words, cur.String())
|
||||
cur.Reset()
|
||||
in = false
|
||||
}
|
||||
default:
|
||||
cur.WriteByte(c)
|
||||
in = true
|
||||
}
|
||||
}
|
||||
if in {
|
||||
words = append(words, cur.String())
|
||||
}
|
||||
return words
|
||||
}
|
||||
Reference in New Issue
Block a user