diff --git a/internal/modules/zellij/zellij.go b/internal/modules/zellij/zellij.go new file mode 100644 index 0000000..d42ba29 --- /dev/null +++ b/internal/modules/zellij/zellij.go @@ -0,0 +1,110 @@ +// Package zellij installs zellij and optionally writes a remote config. +package zellij + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/MDietermann/ArduinoArchSetup/internal/config" + "github.com/MDietermann/ArduinoArchSetup/internal/module" + "github.com/MDietermann/ArduinoArchSetup/internal/platform" +) + +type Module struct { + cfg config.ZellijConfig + pm platform.PackageManager +} + +func New(cfg config.ZellijConfig, pm platform.PackageManager) Module { + return Module{cfg: cfg, pm: pm} +} + +type state struct { + Installed bool + ConfigOK bool + ConfigPath string +} + +func (Module) Meta() module.Meta { + return module.Meta{ID: "zellij", Name: "Zellij terminal multiplexer", Description: "Install zellij and config."} +} + +func configPath() (string, error) { + h, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(h, ".config", "zellij", "config.kdl"), nil +} + +func (m Module) Detect(ctx context.Context) (module.State, error) { + var s state + ok, err := m.pm.IsInstalled(ctx, "zellij") + if err != nil { + return nil, err + } + s.Installed = ok + p, err := configPath() + if err != nil { + return nil, err + } + s.ConfigPath = p + if m.cfg.ConfigURL == "" { + s.ConfigOK = true + } else if _, err := os.Stat(p); err == nil { + s.ConfigOK = true + } + return s, nil +} + +func (m Module) Plan(ctx context.Context, cur module.State) (module.Plan, error) { + s, _ := cur.(state) + var actions []module.Action + if !s.Installed { + actions = append(actions, module.Action{ + Description: "pacman install zellij", + Kind: module.ActionPackageInstall, + Exec: func(ctx context.Context) error { return m.pm.Install(ctx, []string{"zellij"}) }, + }) + } + if !s.ConfigOK { + actions = append(actions, module.Action{ + Description: "download zellij config", + Kind: module.ActionFileWrite, + Exec: func(ctx context.Context) error { + if err := os.MkdirAll(filepath.Dir(s.ConfigPath), 0o755); err != nil { + return err + } + return platform.Download(ctx, m.cfg.ConfigURL, s.ConfigPath, 0o644) + }, + }) + } + return module.Plan{Actions: actions}, nil +} + +func (m Module) Apply(ctx context.Context, p module.Plan, r module.Reporter) error { + for i, a := range p.Actions { + r.Progress(m.Meta().ID, i+1, len(p.Actions), a.Description) + if err := a.Exec(ctx); err != nil { + return err + } + } + return nil +} + +func (m Module) Verify(ctx context.Context) error { + st, err := m.Detect(ctx) + if err != nil { + return err + } + s := st.(state) + if !s.Installed { + return fmt.Errorf("zellij not installed") + } + if !s.ConfigOK { + return fmt.Errorf("zellij config missing") + } + return nil +} diff --git a/internal/modules/zellij/zellij_test.go b/internal/modules/zellij/zellij_test.go new file mode 100644 index 0000000..71d06bf --- /dev/null +++ b/internal/modules/zellij/zellij_test.go @@ -0,0 +1,84 @@ +package zellij + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/MDietermann/ArduinoArchSetup/internal/config" + "github.com/MDietermann/ArduinoArchSetup/internal/module" + "github.com/MDietermann/ArduinoArchSetup/internal/platform" +) + +type fakeRunner struct { + runCalls [][]string + outCalls [][]string + installed map[string]bool +} + +func (f *fakeRunner) Run(_ context.Context, name string, args ...string) error { + f.runCalls = append(f.runCalls, append([]string{name}, args...)) + return nil +} + +func (f *fakeRunner) Output(_ context.Context, name string, args ...string) ([]byte, error) { + f.outCalls = append(f.outCalls, append([]string{name}, args...)) + if name == "pacman" && len(args) == 2 && args[0] == "-Q" { + if f.installed[args[1]] { + return []byte("ok"), nil + } + return nil, errOne + } + return nil, nil +} + +type eo struct{} + +func (eo) Error() string { return "exit 1" } + +var errOne = eo{} + +type nop struct{} + +func (nop) Progress(string, int, int, string) {} +func (nop) Log(string, string) {} + +func TestZellijPlanAppliesInstallAndConfig(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("keybinds {}\n")) + })) + defer srv.Close() + r := &fakeRunner{installed: map[string]bool{}} + m := New(config.ZellijConfig{ConfigURL: srv.URL}, platform.NewPacman(r)) + st, _ := m.Detect(context.Background()) + p, _ := m.Plan(context.Background(), st) + if err := m.Apply(context.Background(), p, nop{}); err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(filepath.Join(home, ".config", "zellij", "config.kdl")) + if err != nil { + t.Fatalf("config not written: %v", err) + } + if string(got) != "keybinds {}\n" { + t.Fatalf("unexpected content %q", got) + } +} + +func TestZellijInstalledPresentEmptyPlan(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + r := &fakeRunner{installed: map[string]bool{"zellij": true}} + m := New(config.ZellijConfig{ConfigURL: ""}, platform.NewPacman(r)) + st, _ := m.Detect(context.Background()) + p, _ := m.Plan(context.Background(), st) + if !p.IsEmpty() { + t.Fatalf("want empty plan, got %+v", p) + } +} + +var _ module.Module = Module{}