mirror of
https://github.com/boolean-maybe/tiki
synced 2026-04-21 13:37:20 +00:00
remove write to config
This commit is contained in:
parent
dcf1b8076a
commit
12fd855c19
8 changed files with 65 additions and 262 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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" {
|
||||
|
|
|
|||
5
main.go
5
main.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue