feat: drop-in support for custom templates
User templates can now be added without rebuilding: drop a script into ~/.config/motd-assist/templates/ and reference it from a preset via "install": "<filename>". Same-name files override built-ins; new files are picked up by the running TUI via 'r'. `motd-assist templates` lists known templates and creates the drop-in directory. Apply plans now embed the resolved template content (Plan.ResolveContents) so the sudo/root process never needs to read user config; snapshots store installed script content so restore works under sudo as well. Validation rejects plans with unresolved content.
This commit is contained in:
parent
d56659e21a
commit
303c49b484
10 changed files with 375 additions and 48 deletions
|
|
@ -1,25 +1,72 @@
|
|||
// Package assets hält die eingebetteten Presets und Custom-Scripte.
|
||||
// Package assets hält die eingebetteten Presets und Custom-Scripte sowie
|
||||
// den Drop-in-Ordner für eigene Vorlagen (~/.config/motd-assist/templates).
|
||||
package assets
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed presets scripts
|
||||
var embedded embed.FS
|
||||
|
||||
// ScriptContent liefert den Inhalt einer gebündelten Script-Vorlage.
|
||||
func ScriptContent(name string) (string, bool) {
|
||||
b, err := embedded.ReadFile("scripts/" + name)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return string(b), true
|
||||
// Template ist eine installierbare Script-Vorlage.
|
||||
type Template struct {
|
||||
Name string
|
||||
Source string // "builtin" oder "user"
|
||||
}
|
||||
|
||||
// ScriptNames listet alle gebündelten Script-Vorlagen.
|
||||
func ScriptNames() []string {
|
||||
// TemplatesDir liefert den Drop-in-Ordner für eigene Vorlagen (XDG-konform).
|
||||
func TemplatesDir() (string, error) {
|
||||
if d := os.Getenv("XDG_CONFIG_HOME"); d != "" {
|
||||
return filepath.Join(d, "motd-assist", "templates"), nil
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, ".config", "motd-assist", "templates"), nil
|
||||
}
|
||||
|
||||
// EnsureTemplatesDir legt den Drop-in-Ordner an, falls er fehlt.
|
||||
func EnsureTemplatesDir() (string, error) {
|
||||
dir, err := TemplatesDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// userTemplates liest den Drop-in-Ordner frisch bei jedem Aufruf, damit
|
||||
// neue Dateien ohne Neustart erkannt werden (TUI: Taste r).
|
||||
func userTemplates() map[string]string {
|
||||
dir, err := TemplatesDir()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(entries))
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
if b, err := os.ReadFile(filepath.Join(dir, e.Name())); err == nil {
|
||||
out[e.Name()] = string(b)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func embeddedScriptNames() []string {
|
||||
entries, err := embedded.ReadDir("scripts")
|
||||
if err != nil {
|
||||
return nil
|
||||
|
|
@ -33,6 +80,78 @@ func ScriptNames() []string {
|
|||
return names
|
||||
}
|
||||
|
||||
func embeddedContent(name string) (string, bool) {
|
||||
b, err := embedded.ReadFile("scripts/" + name)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return string(b), true
|
||||
}
|
||||
|
||||
// Templates listet alle bekannten Vorlagen sortiert. Gleichnamige
|
||||
// User-Vorlagen überdecken Built-ins.
|
||||
func Templates() []Template {
|
||||
user := userTemplates()
|
||||
seen := make(map[string]string, len(user)+4)
|
||||
for name := range user {
|
||||
seen[name] = "user"
|
||||
}
|
||||
for _, name := range embeddedScriptNames() {
|
||||
if _, ok := seen[name]; !ok {
|
||||
seen[name] = "builtin"
|
||||
}
|
||||
}
|
||||
out := make([]Template, 0, len(seen))
|
||||
for name, source := range seen {
|
||||
out = append(out, Template{Name: name, Source: source})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out
|
||||
}
|
||||
|
||||
// ScriptNames listet alle Vorlagen-Namen (Union aus Built-in und Drop-in).
|
||||
func ScriptNames() []string {
|
||||
names := make([]string, 0)
|
||||
for _, t := range Templates() {
|
||||
names = append(names, t.Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// ScriptContent liefert den Inhalt einer Vorlage; User-Vorlagen aus dem
|
||||
// Drop-in-Ordner haben Vorrang vor Built-ins.
|
||||
func ScriptContent(name string) (string, bool) {
|
||||
if c, ok := userTemplates()[name]; ok {
|
||||
return c, true
|
||||
}
|
||||
return embeddedContent(name)
|
||||
}
|
||||
|
||||
// MatchScript findet die Vorlage, deren Inhalt zu content passt, und
|
||||
// erkennt damit bereits installierte motd-assist-Scripte.
|
||||
func MatchScript(content string) (string, bool) {
|
||||
return MatchScriptBytes([]byte(content))
|
||||
}
|
||||
|
||||
// MatchScriptBytes ist die Byte-Variante von MatchScript.
|
||||
func MatchScriptBytes(b []byte) (string, bool) {
|
||||
content := strings.TrimSpace(string(b))
|
||||
if content == "" {
|
||||
return "", false
|
||||
}
|
||||
for name, c := range userTemplates() {
|
||||
if strings.TrimSpace(c) == content {
|
||||
return name, true
|
||||
}
|
||||
}
|
||||
for _, name := range embeddedScriptNames() {
|
||||
if c, ok := embeddedContent(name); ok && strings.TrimSpace(c) == content {
|
||||
return name, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// PresetFiles liefert Dateiname → Inhalt aller eingebetteten Presets.
|
||||
func PresetFiles() map[string]string {
|
||||
entries, err := embedded.ReadDir("presets")
|
||||
|
|
@ -50,23 +169,3 @@ func PresetFiles() map[string]string {
|
|||
}
|
||||
return files
|
||||
}
|
||||
|
||||
// MatchScript findet die Vorlage, deren Inhalt zu content passt, und
|
||||
// erkennt damit bereits installierte motd-assist-Scripte.
|
||||
func MatchScript(content string) (string, bool) {
|
||||
return MatchScriptBytes([]byte(content))
|
||||
}
|
||||
|
||||
// MatchScriptBytes ist die Byte-Variante von MatchScript.
|
||||
func MatchScriptBytes(b []byte) (string, bool) {
|
||||
content := strings.TrimSpace(string(b))
|
||||
if content == "" {
|
||||
return "", false
|
||||
}
|
||||
for _, name := range ScriptNames() {
|
||||
if c, ok := ScriptContent(name); ok && strings.TrimSpace(c) == content {
|
||||
return name, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue