~/.fehbg stops pointing into the predecessor's tree; the session's xinitrc slot runs it once; Go tools set (for the session) and current.
175 lines
4.4 KiB
Go
175 lines
4.4 KiB
Go
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
|
|
}
|