remove write to config

This commit is contained in:
booleanmaybe 2026-04-20 13:59:33 -04:00
parent dcf1b8076a
commit 12fd855c19
8 changed files with 65 additions and 262 deletions

View file

@ -16,9 +16,6 @@ import (
"gopkg.in/yaml.v3"
)
// lastConfigFile tracks the most recently merged config file path for saveConfig().
var lastConfigFile string
// Config holds all application configuration loaded from config.yaml
type Config struct {
Version string `mapstructure:"version"`
@ -70,9 +67,9 @@ func LoadConfig() (*Config, error) {
viper.Reset()
viper.SetConfigType("yaml")
setDefaults()
lastConfigFile = ""
// merge config files in precedence order (first = base, last = highest priority)
merged := 0
for _, path := range findConfigFiles() {
f, err := os.Open(path)
if err != nil {
@ -84,11 +81,11 @@ func LoadConfig() (*Config, error) {
if mergeErr != nil {
return nil, fmt.Errorf("merging config from %s: %w", path, mergeErr)
}
lastConfigFile = path
merged++
slog.Debug("merged configuration", "file", path)
}
if lastConfigFile == "" {
if merged == 0 {
slog.Debug("no config.yaml found, using defaults")
}
@ -254,25 +251,6 @@ func ConvertViewsListToMap(raw map[string]interface{}) {
}
}
// writeWorkflowFile marshals and writes workflow.yaml to the given path.
func writeWorkflowFile(path string, wf *workflowFileData) error {
data, err := yaml.Marshal(wf)
if err != nil {
return fmt.Errorf("marshaling workflow.yaml: %w", err)
}
//nolint:gosec // G306: 0644 is appropriate for config file
if err := os.WriteFile(path, data, 0644); err != nil {
return fmt.Errorf("writing workflow.yaml: %w", err)
}
return nil
}
// GetBoardViewMode loads the board view mode from workflow.yaml.
// Returns "expanded" as default if not found.
func GetBoardViewMode() string {
return getPluginViewModeFromWorkflow("Board", "expanded")
}
// GetPluginViewMode reads a plugin's view mode from workflow.yaml by name.
// Returns empty string if not found.
func GetPluginViewMode(pluginName string) string {
@ -303,57 +281,6 @@ func getPluginViewModeFromWorkflow(pluginName string, defaultValue string) strin
return defaultValue
}
// SavePluginViewMode saves a plugin's view mode to workflow.yaml.
// configIndex: index in workflow.yaml plugins array (-1 to find/create by name)
func SavePluginViewMode(pluginName string, configIndex int, viewMode string) error {
path := FindWorkflowFile()
if path == "" {
// create workflow.yaml in user config dir
path = GetUserConfigWorkflowFile()
}
var wf *workflowFileData
// try to read existing file
if existing, err := readWorkflowFile(path); err == nil {
wf = existing
} else {
wf = &workflowFileData{}
}
if configIndex >= 0 && configIndex < len(wf.Views.Plugins) {
// update existing entry by index
wf.Views.Plugins[configIndex]["view"] = viewMode
} else {
// find by name or create new entry
existingIndex := -1
for i, p := range wf.Views.Plugins {
if name, ok := p["name"].(string); ok && name == pluginName {
existingIndex = i
break
}
}
if existingIndex >= 0 {
wf.Views.Plugins[existingIndex]["view"] = viewMode
} else {
newEntry := map[string]interface{}{
"name": pluginName,
"view": viewMode,
}
wf.Views.Plugins = append(wf.Views.Plugins, newEntry)
}
}
return writeWorkflowFile(path, wf)
}
// SaveHeaderVisible saves the header visibility setting to config.yaml
func SaveHeaderVisible(visible bool) error {
viper.Set("header.visible", visible)
return saveConfig()
}
// GetHeaderVisible returns the header visibility setting
func GetHeaderVisible() bool {
return viper.GetBool("header.visible")
@ -378,16 +305,6 @@ func GetMaxImageRows() int {
return rows
}
// saveConfig writes the current viper configuration to config.yaml.
// Saves to the last merged config file, or the user config dir if none was loaded.
func saveConfig() error {
configFile := lastConfigFile
if configFile == "" {
configFile = GetConfigFile()
}
return viper.WriteConfigAs(configFile)
}
// GetTheme returns the appearance theme setting
func GetTheme() string {
theme := viper.GetString("appearance.theme")

View file

@ -4,8 +4,6 @@ import (
"os"
"path/filepath"
"testing"
"gopkg.in/yaml.v3"
)
func TestLoadConfig(t *testing.T) {
@ -406,144 +404,35 @@ func TestLoadConfigAIAgentDefault(t *testing.T) {
}
}
func TestSavePluginViewMode_PreservesTriggers(t *testing.T) {
func TestGetPluginViewMode_ReadsFromWorkflow(t *testing.T) {
tmpDir := t.TempDir()
// write a workflow.yaml that includes triggers
workflowContent := `statuses:
- key: backlog
label: Backlog
default: true
- key: done
label: Done
done: true
views:
- name: Kanban
default: true
key: "F1"
lanes:
- name: Done
filter: status = 'done'
action: status = 'done'
sort: Priority, CreatedAt
triggers:
- description: block completion with open dependencies
ruki: >
before update
where new.status = "done" and new.dependsOn any status != "done"
deny "cannot complete: has open dependencies"
- description: no jumping from backlog to done
ruki: >
before update
where old.status = "backlog" and new.status = "done"
deny "cannot move directly from backlog to done"
workflowContent := `views:
plugins:
- name: Kanban
key: "F1"
- name: Dependency
view: expanded
`
workflowPath := filepath.Join(tmpDir, "workflow.yaml")
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
if err := os.WriteFile(filepath.Join(tmpDir, "workflow.yaml"), []byte(workflowContent), 0644); err != nil {
t.Fatal(err)
}
// simulate what SavePluginViewMode does: read → modify → write
wf, err := readWorkflowFile(workflowPath)
if err != nil {
t.Fatalf("readWorkflowFile failed: %v", err)
}
originalDir, _ := os.Getwd()
defer func() { _ = os.Chdir(originalDir) }()
_ = os.Chdir(tmpDir)
// modify a view mode (same as SavePluginViewMode logic)
if len(wf.Views.Plugins) > 0 {
wf.Views.Plugins[0]["view"] = "compact"
}
t.Setenv("XDG_CONFIG_HOME", tmpDir)
ResetPathManager()
if err := writeWorkflowFile(workflowPath, wf); err != nil {
t.Fatalf("writeWorkflowFile failed: %v", err)
if got := GetPluginViewMode("Dependency"); got != "expanded" {
t.Errorf("GetPluginViewMode(Dependency) = %q, want %q", got, "expanded")
}
// verify triggers survived the round-trip by reading raw YAML
rawData, err := os.ReadFile(workflowPath)
if err != nil {
t.Fatalf("reading workflow.yaml after write: %v", err)
if got := GetPluginViewMode("Kanban"); got != "" {
t.Errorf("GetPluginViewMode(Kanban) = %q, want empty (no view field)", got)
}
var raw map[string]interface{}
if err := yaml.Unmarshal(rawData, &raw); err != nil {
t.Fatalf("parsing raw YAML: %v", err)
}
triggers, ok := raw["triggers"]
if !ok {
t.Fatal("triggers section missing after round-trip write")
}
triggerList, ok := triggers.([]interface{})
if !ok {
t.Fatalf("triggers is not a list, got %T", triggers)
}
if len(triggerList) != 2 {
t.Fatalf("expected 2 triggers after round-trip, got %d", len(triggerList))
}
// also verify via typed struct
wf2, err := readWorkflowFile(workflowPath)
if err != nil {
t.Fatalf("readWorkflowFile after write failed: %v", err)
}
if len(wf2.Triggers) != 2 {
t.Fatalf("expected 2 triggers in struct after round-trip, got %d", len(wf2.Triggers))
}
desc0, _ := wf2.Triggers[0]["description"].(string)
if desc0 != "block completion with open dependencies" {
t.Errorf("trigger[0] description = %q, want %q", desc0, "block completion with open dependencies")
}
}
func TestSavePluginViewMode_PreservesDescription(t *testing.T) {
tmpDir := t.TempDir()
workflowContent := `description: |
Release workflow. Coordinate feature rollout through
Planned Building Staging Canary Released.
statuses:
- key: backlog
label: Backlog
default: true
- key: done
label: Done
done: true
views:
- name: Kanban
default: true
key: "F1"
lanes:
- name: Done
filter: status = 'done'
action: status = 'done'
sort: Priority, CreatedAt
`
workflowPath := filepath.Join(tmpDir, "workflow.yaml")
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
t.Fatal(err)
}
wf, err := readWorkflowFile(workflowPath)
if err != nil {
t.Fatalf("readWorkflowFile failed: %v", err)
}
wantDesc := "Release workflow. Coordinate feature rollout through\nPlanned → Building → Staging → Canary → Released.\n"
if wf.Description != wantDesc {
t.Errorf("description after read = %q, want %q", wf.Description, wantDesc)
}
if len(wf.Views.Plugins) > 0 {
wf.Views.Plugins[0]["view"] = "compact"
}
if err := writeWorkflowFile(workflowPath, wf); err != nil {
t.Fatalf("writeWorkflowFile failed: %v", err)
}
wf2, err := readWorkflowFile(workflowPath)
if err != nil {
t.Fatalf("readWorkflowFile after write failed: %v", err)
}
if wf2.Description != wantDesc {
t.Errorf("description after round-trip = %q, want %q", wf2.Description, wantDesc)
if got := GetPluginViewMode("NonExistent"); got != "" {
t.Errorf("GetPluginViewMode(NonExistent) = %q, want empty", got)
}
}

View file

@ -279,10 +279,14 @@ func (ir *InputRouter) openDepsEditor(taskID string) bool {
},
}
if vm := config.GetPluginViewMode("Dependency"); vm != "" {
pluginDef.ViewMode = vm
}
pluginConfig := model.NewPluginConfig("Dependency")
pluginConfig.SetLaneLayout([]int{1, 2, 1}, []int{25, 50, 25})
if vm := config.GetPluginViewMode("Dependency"); vm != "" {
pluginConfig.SetViewMode(vm)
if pluginDef.ViewMode == "expanded" {
pluginConfig.SetViewMode("expanded")
}
ctrl := NewDepsController(ir.taskStore, ir.mutationGate, pluginConfig, pluginDef, ir.navController, ir.statusline, ir.schema)

View file

@ -45,7 +45,6 @@ func BuildPluginConfigsAndDefs(plugins []plugin.Plugin) (map[string]*model.Plugi
pluginDefs := make(map[string]plugin.Plugin)
for _, p := range plugins {
pc := model.NewPluginConfig(p.GetName())
pc.SetConfigIndex(p.GetConfigIndex()) // Pass ConfigIndex for saving view mode changes
if tp, ok := p.(*plugin.TikiPlugin); ok {
if tp.ViewMode == "expanded" {

View file

@ -135,11 +135,6 @@ func main() {
os.Exit(1)
}
// Save user preferences on shutdown
if err := config.SaveHeaderVisible(result.HeaderConfig.GetUserPreference()); err != nil {
slog.Warn("failed to save header visibility preference", "error", err)
}
// Keep logLevel variable referenced so it isn't optimized away in some builds
_ = result.LogLevel
}

View file

@ -4,7 +4,6 @@ import (
"log/slog"
"sync"
"github.com/boolean-maybe/tiki/config"
"github.com/boolean-maybe/tiki/task"
)
@ -31,7 +30,6 @@ type PluginConfig struct {
preSearchLane int
preSearchIndices []int
viewMode ViewMode // compact or expanded display
configIndex int // index in workflow.yaml views array (-1 if not from a config file)
listeners map[int]PluginSelectionListener
nextListenerID int
searchState SearchState // search state (embedded)
@ -42,21 +40,13 @@ func NewPluginConfig(name string) *PluginConfig {
pc := &PluginConfig{
pluginName: name,
viewMode: ViewModeCompact,
configIndex: -1, // Default to -1 (not in config)
listeners: make(map[int]PluginSelectionListener),
nextListenerID: 1, // Start at 1 to avoid conflict with zero-value sentinel
nextListenerID: 1,
}
pc.SetLaneLayout([]int{4}, nil)
return pc
}
// SetConfigIndex sets the config index for this plugin
func (pc *PluginConfig) SetConfigIndex(index int) {
pc.mu.Lock()
defer pc.mu.Unlock()
pc.configIndex = index
}
// GetPluginName returns the plugin name
func (pc *PluginConfig) GetPluginName() string {
return pc.pluginName
@ -280,16 +270,8 @@ func (pc *PluginConfig) ToggleViewMode() {
} else {
pc.viewMode = ViewModeCompact
}
newMode := pc.viewMode
pluginName := pc.pluginName
configIndex := pc.configIndex
pc.mu.Unlock()
// Save to config (same pattern as BoardConfig)
if err := config.SavePluginViewMode(pluginName, configIndex, string(newMode)); err != nil {
slog.Error("failed to save plugin view mode", "plugin", pluginName, "error", err)
}
pc.notifyListeners()
}

View file

@ -1,6 +1,7 @@
package model
import (
"os"
"sync"
"testing"
"time"
@ -285,9 +286,6 @@ func TestPluginConfig_ViewMode(t *testing.T) {
func TestPluginConfig_ToggleViewMode(t *testing.T) {
pc := NewPluginConfig("test")
// Note: ToggleViewMode calls config.SavePluginViewMode which will fail in tests
// but should not affect the toggle logic
initial := pc.GetViewMode()
// Toggle
@ -311,6 +309,41 @@ func TestPluginConfig_ToggleViewMode(t *testing.T) {
}
}
func TestPluginConfig_ToggleViewMode_SessionOnly(t *testing.T) {
tmpDir := t.TempDir()
workflowContent := `views:
plugins:
- name: TestPlugin
view: compact
`
workflowPath := tmpDir + "/workflow.yaml"
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
t.Fatal(err)
}
info, err := os.Stat(workflowPath)
if err != nil {
t.Fatal(err)
}
modBefore := info.ModTime()
sizeBefore := info.Size()
pc := NewPluginConfig("TestPlugin")
pc.ToggleViewMode()
if pc.GetViewMode() != ViewModeExpanded {
t.Fatal("expected expanded after toggle")
}
info, err = os.Stat(workflowPath)
if err != nil {
t.Fatal(err)
}
if info.ModTime() != modBefore || info.Size() != sizeBefore {
t.Error("ToggleViewMode must not write to workflow.yaml")
}
}
func TestPluginConfig_SearchState(t *testing.T) {
pc := NewPluginConfig("test")
@ -546,21 +579,6 @@ func TestPluginConfig_ConcurrentAccess(t *testing.T) {
// If we get here without panic, test passes
}
func TestPluginConfig_SetConfigIndex(t *testing.T) {
pc := NewPluginConfig("test")
// SetConfigIndex doesn't have a getter, but we're testing it doesn't panic
pc.SetConfigIndex(5)
pc.SetConfigIndex(-1)
pc.SetConfigIndex(0)
// Verify it doesn't affect other operations
pc.SetSelectedIndex(3)
if pc.GetSelectedIndex() != 3 {
t.Error("SetConfigIndex affected GetSelectedIndex")
}
}
func TestPluginConfig_GridNavigation_PartialLastRow(t *testing.T) {
pc := NewPluginConfig("test")

View file

@ -375,7 +375,6 @@ func (ta *TestApp) LoadPlugins() error {
for _, p := range plugins {
pc := model.NewPluginConfig(p.GetName())
pc.SetConfigIndex(p.GetConfigIndex())
pluginConfigs[p.GetName()] = pc
// Create appropriate controller based on plugin type