mesh/merge-gate out of date: judged against trunk 2fe5601397ae, the trunk is now 0863ec2a059d; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 2fe5601397ae, the trunk is now 0863ec2a059d; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
A module that installs Betterbird but leaves every node to its own settings does not set Betterbird up. The operator approved the configuration on 2026-10-12, and it now ships in three layers. The policy locks what must never differ: telemetry, add-on updates, the vendor's notices and the start page. A defaults file carries the laptop's setup, so a fresh node gets it untouched. The setting prefs stays a node's exceptions. A default loses to a value the profile saved. betterbird_config shows that drift, betterbird_check reports it, and betterbird_config_apply clears it while Betterbird is quit. keep proposes a node layer instead of writing user.js. betterbird_prefs now reads Betterbird's real defaults from omni.ja, and withholds of mailnews.* only what names an account. The default-client ask is decided from its own policy entry, not from the first thing the policy lacks.
533 lines
19 KiB
Go
533 lines
19 KiB
Go
package main
|
|
|
|
// The configuration the module ships (novox/hq issue 345), in three layers:
|
|
//
|
|
// - LOCKED: the enterprise policy (/opt/betterbird/distribution/policies.json). Its Preferences entries with
|
|
// Status locked win over everything, and Betterbird's settings show them greyed out.
|
|
// - DEFAULT: the module's defaults file (/opt/betterbird/defaults/pref/mesh-defaults.js), the operator's
|
|
// setup. Betterbird reads it as a default, so a value the profile saved in prefs.js wins over it: that is
|
|
// drift, unless a node layer says so.
|
|
// - NODE: the setting prefs, which the node-engine writes to ~/.config/betterbird/user.js and the tools place
|
|
// in the profile's user.js, where it wins over prefs.js at every start.
|
|
//
|
|
// betterbird_config shows each managed preference through the layers and flags the drift; betterbird_config_apply
|
|
// clears a drifting value from prefs.js while Betterbird is not running; keep proposes a node layer that holds the
|
|
// profile's value for this node instead, through the controller's settings verb, never by writing user.js.
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// prefDir is the installation's own defaults directory; the module places its files there.
|
|
prefDir = installDir + "/defaults/pref"
|
|
// meshDefaults is the module's DEFAULT layer.
|
|
meshDefaults = prefDir + "/mesh-defaults.js"
|
|
// omniJa is Betterbird's packed application, holding its own default preferences.
|
|
omniJa = installDir + "/omni.ja"
|
|
)
|
|
|
|
// moduleFiles are the module's files in the installation's defaults directory: their values are the module's
|
|
// defaults, not Betterbird's.
|
|
var moduleFiles = map[string]bool{"mesh-defaults.js": true, "vendor-prefs.js": true}
|
|
|
|
var (
|
|
// defaultLine is one pref(…) or sticky_pref(…) line of a defaults file; a comment may follow it.
|
|
defaultLine = regexp.MustCompile(`^\s*(?:sticky_)?pref\(\s*"((?:[^"\\]|\\.)*)"\s*,\s*(.*?)\s*\)\s*;`)
|
|
// keepsDefault is a line of the module's defaults naming a preference it manages without a value.
|
|
keepsDefault = regexp.MustCompile(`^//\s*keeps Betterbird's default:\s*([A-Za-z0-9_.@{}\-]+)\s*$`)
|
|
)
|
|
|
|
// nodeName is the machine this runtime serves, as the mesh names it: a variable so a test answers.
|
|
var nodeName = func() string { return os.Getenv("MESH_NODE") }
|
|
|
|
// shipped is the module's DEFAULT layer as placed on the machine.
|
|
type shipped struct {
|
|
values map[string]string
|
|
// keeps are the managed names whose module value is Betterbird's own default.
|
|
keeps map[string]bool
|
|
// order is every managed name, in the file's order.
|
|
order []string
|
|
}
|
|
|
|
// parseShipped reads the module's defaults file.
|
|
func parseShipped(raw []byte) shipped {
|
|
s := shipped{values: map[string]string{}, keeps: map[string]bool{}}
|
|
for _, line := range strings.Split(string(raw), "\n") {
|
|
if mm := keepsDefault.FindStringSubmatch(strings.TrimSpace(line)); mm != nil {
|
|
if !s.keeps[mm[1]] {
|
|
s.keeps[mm[1]] = true
|
|
s.order = append(s.order, mm[1])
|
|
}
|
|
continue
|
|
}
|
|
if mm := defaultLine.FindStringSubmatch(line); mm != nil {
|
|
if _, seen := s.values[mm[1]]; !seen {
|
|
s.order = append(s.order, mm[1])
|
|
}
|
|
s.values[mm[1]] = mm[2]
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// shippedDefaults reads the module's defaults file from the installation.
|
|
func (m *Machine) shippedDefaults() (shipped, error) {
|
|
raw, err := readBounded(m.path(meshDefaults))
|
|
if err != nil {
|
|
return shipped{values: map[string]string{}, keeps: map[string]bool{}}, err
|
|
}
|
|
return parseShipped(raw), nil
|
|
}
|
|
|
|
// DefaultValue is a default preference and the file that gives it.
|
|
type DefaultValue struct {
|
|
Value string
|
|
From string
|
|
// Module says the module's files give it, not Betterbird's.
|
|
Module bool
|
|
}
|
|
|
|
// readDefaultsInto reads the pref(…) lines of the files of one place, in Gecko's order: it reads a place's
|
|
// files last-first by name (Preferences.cpp, pref_ReadDefaultPrefs), so of a name in several files the value
|
|
// of the file first by name holds. A later place overrides an earlier one.
|
|
func readDefaultsInto(out map[string]DefaultValue, files map[string][]byte, label func(string) (string, bool)) {
|
|
names := make([]string, 0, len(files))
|
|
for n := range files {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
place := map[string]DefaultValue{}
|
|
for i := len(names) - 1; i >= 0; i-- {
|
|
from, module := label(names[i])
|
|
for _, line := range strings.Split(string(files[names[i]]), "\n") {
|
|
if mm := defaultLine.FindStringSubmatch(line); mm != nil {
|
|
place[mm[1]] = DefaultValue{Value: mm[2], From: from, Module: module}
|
|
}
|
|
}
|
|
}
|
|
for k, v := range place {
|
|
out[k] = v
|
|
}
|
|
}
|
|
|
|
// mostDefaultsFile bounds one defaults file read from omni.ja.
|
|
const mostDefaultsFile = 4 << 20
|
|
|
|
// defaults reads every default preference Betterbird starts with: greprefs.js and defaults/pref/*.js in
|
|
// omni.ja, then the installation's defaults/pref/*.js, where the module's files are. A preference compiled into
|
|
// Betterbird (a static preference) is in no file, and is absent here. withModule false leaves the module's own
|
|
// files out, so what remains is Betterbird's. The error says what could not be read.
|
|
func (m *Machine) defaults(withModule bool) (map[string]DefaultValue, error) {
|
|
out := map[string]DefaultValue{}
|
|
var problems []string
|
|
if r, err := zip.OpenReader(m.path(omniJa)); err != nil {
|
|
problems = append(problems, omniJa+" is unreadable: "+err.Error())
|
|
} else {
|
|
gre, app := map[string][]byte{}, map[string][]byte{}
|
|
for _, f := range r.File {
|
|
switch {
|
|
case f.Name == "greprefs.js":
|
|
case strings.HasPrefix(f.Name, "defaults/pref/") && strings.HasSuffix(f.Name, ".js") && !strings.Contains(f.Name[len("defaults/pref/"):], "/"):
|
|
default:
|
|
continue
|
|
}
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
b, _ := io.ReadAll(io.LimitReader(rc, mostDefaultsFile))
|
|
rc.Close()
|
|
if f.Name == "greprefs.js" {
|
|
gre[f.Name] = b
|
|
} else {
|
|
app[filepath.Base(f.Name)] = b
|
|
}
|
|
}
|
|
r.Close()
|
|
readDefaultsInto(out, gre, func(n string) (string, bool) { return "Betterbird's default (omni.ja " + n + ")", false })
|
|
readDefaultsInto(out, app, func(n string) (string, bool) {
|
|
return "Betterbird's default (omni.ja defaults/pref/" + n + ")", false
|
|
})
|
|
}
|
|
installed := map[string][]byte{}
|
|
entries, _ := os.ReadDir(m.path(prefDir))
|
|
for _, e := range entries {
|
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".js") || (moduleFiles[e.Name()] && !withModule) {
|
|
continue
|
|
}
|
|
if b, err := readBounded(m.path(filepath.Join(prefDir, e.Name()))); err == nil {
|
|
installed[e.Name()] = b
|
|
}
|
|
}
|
|
readDefaultsInto(out, installed, func(n string) (string, bool) {
|
|
if moduleFiles[n] {
|
|
return "the module's default (" + prefDir + "/" + n + ")", true
|
|
}
|
|
return "Betterbird's default (" + prefDir + "/" + n + ")", false
|
|
})
|
|
if len(problems) > 0 {
|
|
return out, errors.New(strings.Join(problems, "; "))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// policyPrefs are the Preferences entries of the placed policy: name to its value as a preference file writes
|
|
// it, and whether it is locked. An unreadable policy has none.
|
|
type policyPref struct {
|
|
Value string
|
|
Locked bool
|
|
}
|
|
|
|
func policyPrefsOf(raw []byte) map[string]policyPref {
|
|
var p struct {
|
|
Policies struct {
|
|
Preferences map[string]struct {
|
|
Value any `json:"Value"`
|
|
Status string `json:"Status"`
|
|
} `json:"Preferences"`
|
|
} `json:"policies"`
|
|
}
|
|
out := map[string]policyPref{}
|
|
if json.Unmarshal(raw, &p) != nil {
|
|
return out
|
|
}
|
|
for name, e := range p.Policies.Preferences {
|
|
out[name] = policyPref{Value: literal(e.Value), Locked: e.Status == "locked"}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// locks are the preferences the placed policy locks: name to value.
|
|
func (m *Machine) locks() map[string]string {
|
|
raw, err := readBounded(m.path(policyFile))
|
|
if err != nil {
|
|
return map[string]string{}
|
|
}
|
|
out := map[string]string{}
|
|
for name, p := range policyPrefsOf(raw) {
|
|
if p.Locked {
|
|
out[name] = p.Value
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// literal is a JSON value as a preference file writes it.
|
|
func literal(v any) string {
|
|
switch x := v.(type) {
|
|
case bool:
|
|
return strconv.FormatBool(x)
|
|
case float64:
|
|
return strconv.FormatFloat(x, 'f', -1, 64)
|
|
case string:
|
|
return quoted(x)
|
|
case nil:
|
|
return ""
|
|
}
|
|
b, _ := json.Marshal(v)
|
|
return string(b)
|
|
}
|
|
|
|
// statementsOf are the user_pref(…) statements of a user.js, several to a line allowed: name to value.
|
|
func statementsOf(raw []byte) map[string]string {
|
|
out := map[string]string{}
|
|
for _, line := range strings.Split(string(raw), "\n") {
|
|
for _, st := range splitStatements(strings.TrimSpace(line)) {
|
|
if mm := prefLine.FindStringSubmatch(st); mm != nil {
|
|
out[mm[1]] = mm[2]
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ConfigPref is one managed preference through the layers.
|
|
type ConfigPref struct {
|
|
Name string `json:"name"`
|
|
// Layer is locked (the policy), default (the module's defaults file) or "Betterbird's default" (a managed
|
|
// preference whose module value is Betterbird's own).
|
|
Layer string `json:"layer"`
|
|
Locked string `json:"locked,omitempty"`
|
|
ModuleDefault string `json:"module_default,omitempty"`
|
|
BetterbirdDefault string `json:"betterbird_default,omitempty"`
|
|
// NodeLayer is what the setting prefs on this machine says (the module's user.js).
|
|
NodeLayer string `json:"node_layer,omitempty"`
|
|
// Profile is what the profile saved in its prefs.js.
|
|
Profile string `json:"profile,omitempty"`
|
|
Effective string `json:"effective"`
|
|
From string `json:"effective_from"`
|
|
// Drift says how the profile's own value beats the module's without a node layer saying so.
|
|
Drift string `json:"drift,omitempty"`
|
|
// module is the value the module means: its default, or Betterbird's.
|
|
module string
|
|
}
|
|
|
|
// ConfigAnswer is what betterbird_config answers.
|
|
type ConfigAnswer struct {
|
|
Profile string `json:"profile"`
|
|
Defaults string `json:"module_defaults"`
|
|
Prefs []ConfigPref `json:"prefs"`
|
|
// Drifting are the names that drift.
|
|
Drifting []string `json:"drifting"`
|
|
// Proposed is the controller's answer to a node layer proposed with keep.
|
|
Proposed string `json:"proposed,omitempty"`
|
|
Note string `json:"note,omitempty"`
|
|
}
|
|
|
|
// config reads every managed preference through the layers: the policy's locks, the module's defaults, the
|
|
// node layer (the declared user.js), the profile's prefs.js and user.js.
|
|
func (m *Machine) config() (ConfigAnswer, Profile, error) {
|
|
a := ConfigAnswer{Prefs: []ConfigPref{}, Drifting: []string{}}
|
|
p, err := m.profile()
|
|
if err != nil {
|
|
return a, p, err
|
|
}
|
|
a.Profile = p.Dir
|
|
sh, serr := m.shippedDefaults()
|
|
if serr != nil {
|
|
a.Defaults = "not placed: " + meshDefaults + " is missing; the module's push places it"
|
|
} else {
|
|
a.Defaults = fmt.Sprintf("placed: %s, %d preference(s) with a value and %d kept at Betterbird's default",
|
|
meshDefaults, len(sh.values), len(sh.keeps))
|
|
}
|
|
locked := m.locks()
|
|
own, _ := m.defaults(false)
|
|
declaredRaw, _ := readBounded(m.home(declared))
|
|
node := statementsOf(declaredRaw)
|
|
prefsRaw, _ := readBounded(filepath.Join(p.path, "prefs.js"))
|
|
saved := readPrefs(prefsRaw)
|
|
userRaw, _ := readBounded(filepath.Join(p.path, "user.js"))
|
|
placed := statementsOf(userRaw)
|
|
|
|
var names []string
|
|
for n := range locked {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
for _, n := range sh.order {
|
|
if _, isLocked := locked[n]; !isLocked {
|
|
names = append(names, n)
|
|
}
|
|
}
|
|
for _, n := range names {
|
|
c := ConfigPref{Name: n, NodeLayer: node[n], Profile: saved[n]}
|
|
bb, hasBB := own[n]
|
|
if hasBB {
|
|
c.BetterbirdDefault = bb.Value
|
|
}
|
|
switch v, isLocked := locked[n]; {
|
|
case isLocked:
|
|
c.Layer, c.Locked, c.module = "locked", v, v
|
|
case sh.keeps[n]:
|
|
c.Layer = "Betterbird's default"
|
|
c.module = c.BetterbirdDefault
|
|
default:
|
|
c.Layer, c.ModuleDefault, c.module = "default", sh.values[n], sh.values[n]
|
|
}
|
|
switch {
|
|
case c.Locked != "":
|
|
c.Effective, c.From = c.Locked, "locked by the module's policy"
|
|
case placed[n] != "":
|
|
c.Effective, c.From = placed[n], "the profile's user.js (wins at the start)"
|
|
case c.Profile != "":
|
|
c.Effective, c.From = c.Profile, "the profile's prefs.js"
|
|
case c.ModuleDefault != "":
|
|
c.Effective, c.From = c.ModuleDefault, "the module's default"
|
|
case c.BetterbirdDefault != "":
|
|
c.Effective, c.From = c.BetterbirdDefault, bb.From
|
|
default:
|
|
c.From = "Betterbird's built-in default (in no defaults file)"
|
|
}
|
|
if why := drift(c); why != "" {
|
|
c.Drift = why
|
|
a.Drifting = append(a.Drifting, n)
|
|
}
|
|
c.NodeLayer, c.Profile, c.Effective = m.maskValue(c.NodeLayer), m.maskValue(c.Profile), m.maskValue(c.Effective)
|
|
a.Prefs = append(a.Prefs, c)
|
|
}
|
|
return a, p, nil
|
|
}
|
|
|
|
// drift says how the profile's saved value beats the module's, or "": a value in prefs.js that is not the
|
|
// module's, where no lock holds and no node layer names the preference.
|
|
func drift(c ConfigPref) string {
|
|
if c.Locked != "" || c.NodeLayer != "" || c.Profile == "" || c.Profile == c.module {
|
|
return ""
|
|
}
|
|
want := c.module
|
|
if want == "" {
|
|
want = "Betterbird's built-in default"
|
|
}
|
|
return fmt.Sprintf("the profile's prefs.js holds %s, which beats the module's %s, and no node layer says so", c.Profile, want)
|
|
}
|
|
|
|
// Config is what betterbird_config answers; with keep it proposes a node layer of the setting prefs that holds
|
|
// the profile's own values of the named managed preferences on this machine (novox/hq issue 345). The module
|
|
// never writes user.js for it: the operator approves the layer, and the next push writes it.
|
|
func (m *Machine) Config(keep []string) (ConfigAnswer, error) {
|
|
a, p, err := m.config()
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
a.Note = "drift is a value the profile saved that beats the module's default without a node layer saying so: " +
|
|
"betterbird_config_apply clears it (Betterbird quit), or keep proposes a node layer that holds it for this machine"
|
|
if len(keep) == 0 {
|
|
return a, nil
|
|
}
|
|
node := nodeName()
|
|
if node == "" {
|
|
return a, errors.New("nothing was proposed: this runtime does not say which machine it serves (MESH_NODE)")
|
|
}
|
|
by := map[string]ConfigPref{}
|
|
for _, c := range a.Prefs {
|
|
by[c.Name] = c
|
|
}
|
|
prefsRaw, _ := readBounded(filepath.Join(p.path, "prefs.js"))
|
|
saved := readPrefs(prefsRaw)
|
|
declaredRaw, err := readBounded(m.home(declared))
|
|
if err != nil {
|
|
return a, fmt.Errorf("nothing was proposed: the module's user.js is not on this machine (~/%s)", declared)
|
|
}
|
|
kept := map[string]string{}
|
|
for _, n := range keep {
|
|
c, managed := by[n]
|
|
switch {
|
|
case !managed:
|
|
return a, fmt.Errorf("nothing was proposed: %s is not a preference the module manages (betterbird_config lists them)", n)
|
|
case c.Locked != "":
|
|
return a, fmt.Errorf("nothing was proposed: %s is locked by the module's policy; a lock changes only in the catalogue", n)
|
|
case saved[n] == "":
|
|
return a, fmt.Errorf("nothing was proposed: the profile saved no value of its own for %s", n)
|
|
}
|
|
kept[n] = saved[n]
|
|
}
|
|
// The setting is one string, so a node layer replaces it whole: it carries what the setting says on this
|
|
// machine today, with the kept values in place of any the same names had.
|
|
var parts []string
|
|
for _, line := range strings.Split(string(declaredRaw), "\n") {
|
|
for _, st := range splitStatements(strings.TrimSpace(line)) {
|
|
if mm := prefLine.FindStringSubmatch(st); mm != nil {
|
|
if _, replaced := kept[mm[1]]; !replaced {
|
|
parts = append(parts, fmt.Sprintf("user_pref(%s, %s);", quoted(mm[1]), mm[2]))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, n := range keep {
|
|
if v, ok := kept[n]; ok {
|
|
parts = append(parts, fmt.Sprintf("user_pref(%s, %s);", quoted(n), v))
|
|
delete(kept, n)
|
|
}
|
|
}
|
|
value := strings.Join(parts, " ")
|
|
if err := validUserJS(mark + ")\n" + value + "\n"); err != nil {
|
|
return a, fmt.Errorf("nothing was proposed: the layer would be refused: %v", err)
|
|
}
|
|
body, _ := json.Marshal(map[string]string{"prefs": value})
|
|
raw, err := asker("seat:mesh-controller.settings", map[string]any{"module": "betterbird", "node": node,
|
|
"values": string(body), "propose": "true"})
|
|
if err != nil {
|
|
return a, fmt.Errorf("the controller's settings verb did not answer: %v", err)
|
|
}
|
|
said, ok := verbSaid(raw)
|
|
a.Proposed = m.maskValue(said)
|
|
if !ok {
|
|
return a, fmt.Errorf("the controller did not take the proposal: %s", a.Proposed)
|
|
}
|
|
a.Note = "proposed for " + node + ": the operator approves it on a channel that proves who answers; the next " +
|
|
"push writes it, and it is placed at Betterbird's next start from the launcher (or betterbird_restart). " +
|
|
"The layer carries the whole setting prefs as it stands on this machine, since a layer replaces it whole"
|
|
return a, nil
|
|
}
|
|
|
|
// Cleared is one value betterbird_config_apply took out of prefs.js.
|
|
type Cleared struct {
|
|
Name string `json:"name"`
|
|
Was string `json:"was"`
|
|
// Now is the value that holds once Betterbird starts again.
|
|
Now string `json:"now"`
|
|
}
|
|
|
|
// ConfigApplyAnswer is what betterbird_config_apply answers.
|
|
type ConfigApplyAnswer struct {
|
|
Profile string `json:"profile"`
|
|
Applied bool `json:"applied"`
|
|
Clears []Cleared `json:"clears"`
|
|
Note string `json:"note"`
|
|
}
|
|
|
|
// ConfigApply takes the drifting values out of the profile's prefs.js, so the module's value holds at the next
|
|
// start. Without apply it lists them. It acts only while Betterbird is not running, because Betterbird
|
|
// rewrites prefs.js when it quits. Every other line of prefs.js is kept as it was.
|
|
func (m *Machine) ConfigApply(apply bool) (ConfigApplyAnswer, error) {
|
|
c, p, err := m.config()
|
|
a := ConfigApplyAnswer{Profile: c.Profile, Clears: []Cleared{}}
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
if strings.HasPrefix(c.Defaults, "not placed") {
|
|
return a, fmt.Errorf("the module's defaults are %s: nothing to bring the profile back to", c.Defaults)
|
|
}
|
|
drifting := map[string]bool{}
|
|
for _, cp := range c.Prefs {
|
|
if cp.Drift != "" {
|
|
drifting[cp.Name] = true
|
|
now := m.maskValue(cp.module)
|
|
if now == "" {
|
|
now = "Betterbird's built-in default"
|
|
}
|
|
a.Clears = append(a.Clears, Cleared{Name: cp.Name, Was: cp.Profile, Now: now})
|
|
}
|
|
}
|
|
if !apply {
|
|
a.Note = "a dry run: these values would be taken out of prefs.js. Quit Betterbird, then call with apply"
|
|
return a, nil
|
|
}
|
|
if ps := m.mainProcs(); len(ps) > 0 {
|
|
return a, fmt.Errorf("Betterbird is running (pid %d): it rewrites prefs.js when it quits, so nothing was cleared; quit it, then apply again", ps[0].PID)
|
|
}
|
|
if len(drifting) == 0 {
|
|
a.Applied, a.Note = true, "nothing drifts: prefs.js was left as it was"
|
|
return a, nil
|
|
}
|
|
target := filepath.Join(p.path, "prefs.js")
|
|
raw, err := readBounded(target)
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
st, err := os.Stat(target)
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
lines := strings.SplitAfter(string(raw), "\n")
|
|
var kept strings.Builder
|
|
for _, l := range lines {
|
|
if mm := prefLine.FindStringSubmatch(strings.TrimRight(l, "\n")); mm != nil && drifting[mm[1]] {
|
|
continue
|
|
}
|
|
kept.WriteString(l)
|
|
}
|
|
tmp := target + ".mesh-new"
|
|
if err := os.WriteFile(tmp, []byte(kept.String()), st.Mode().Perm()); err != nil {
|
|
return a, err
|
|
}
|
|
if err := os.Rename(tmp, target); err != nil {
|
|
os.Remove(tmp)
|
|
return a, err
|
|
}
|
|
a.Applied = true
|
|
a.Note = fmt.Sprintf("cleared %d value(s) from prefs.js; the module's values hold from Betterbird's next start", len(a.Clears))
|
|
return a, nil
|
|
}
|