From 5c71d7bc11ace64f7d96be07fba0fa7e3dae843f Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Sun, 8 Feb 2026 23:27:13 +0200 Subject: [PATCH] feat: config name case insensitivity --- internal/cli/attach_cmd.go | 4 +- internal/cli/main_cmd.go | 9 ++-- internal/cli/main_cmd_test.go | 35 +++++++++++++ internal/cli/remove_cmd.go | 5 +- internal/cli/show_cmd.go | 4 +- internal/config/types.go | 19 +++++++ internal/config/types_test.go | 96 +++++++++++++++++++++++++++++++++++ internal/config/writer.go | 2 +- 8 files changed, 163 insertions(+), 11 deletions(-) diff --git a/internal/cli/attach_cmd.go b/internal/cli/attach_cmd.go index cdf339d..7fe6f89 100644 --- a/internal/cli/attach_cmd.go +++ b/internal/cli/attach_cmd.go @@ -33,12 +33,12 @@ func runAttach(cmd *cobra.Command, args []string) error { return err } - item, exists := allConfig[key] + item, actualKey, exists := allConfig.Get(key) if !exists { return NewUserError("tmux config item '" + key + "' not found") } - parsed := config.ParseConfig(key, item) + parsed := config.ParseConfig(actualKey, item) if !tmux.SessionExists(opts, parsed.Name) { return NewUserError("tmux session '" + parsed.Name + "' does not exist") diff --git a/internal/cli/main_cmd.go b/internal/cli/main_cmd.go index 3a9314e..d913fe6 100644 --- a/internal/cli/main_cmd.go +++ b/internal/cli/main_cmd.go @@ -34,10 +34,11 @@ func runMain(cmd *cobra.Command, args []string) error { return err } - if _, exists := info.Merged.Config[selected]; !exists { + if _, actualKey, exists := info.Merged.Config.Get(selected); !exists { return NewUserError("tmux config item '" + selected + "' not found") + } else { + key = actualKey } - key = selected } // Get config @@ -46,12 +47,12 @@ func runMain(cmd *cobra.Command, args []string) error { return err } - item, exists := allConfig[key] + item, actualKey, exists := allConfig.Get(key) if !exists { return NewUserError("tmux config item '" + key + "' not found") } - parsed := config.ParseConfig(key, item) + parsed := config.ParseConfig(actualKey, item) // Check if session exists if tmux.SessionExists(opts, parsed.Name) { diff --git a/internal/cli/main_cmd_test.go b/internal/cli/main_cmd_test.go index 3ce6a2a..d5c4617 100644 --- a/internal/cli/main_cmd_test.go +++ b/internal/cli/main_cmd_test.go @@ -58,6 +58,41 @@ testproject: } } +func TestRunMain_CaseInsensitiveKey(t *testing.T) { + // Create a temp directory with a config file + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, ".tmux.yaml") + + content := ` +Notes: + root: /tmp/notes + windows: + - ./src +` + err := os.WriteFile(configPath, []byte(content), 0644) + if err != nil { + t.Fatalf("failed to write temp config: %v", err) + } + + // Set XDG_CONFIG_HOME to temp directory so config is found + oldXDG := os.Getenv("XDG_CONFIG_HOME") + _ = os.Setenv("XDG_CONFIG_HOME", tmpDir) + defer func() { _ = os.Setenv("XDG_CONFIG_HOME", oldXDG) }() + + dry = true + defer func() { dry = false }() + + // Should succeed with different casings + for _, key := range []string{"Notes", "notes", "NOTES", "nOtEs"} { + t.Run(key, func(t *testing.T) { + err := runMain(nil, []string{key}) + if err != nil { + t.Errorf("expected no error for key %q, got %v", key, err) + } + }) + } +} + func TestRunMain_InvalidKey(t *testing.T) { // Create a temp directory with a config file tmpDir := t.TempDir() diff --git a/internal/cli/remove_cmd.go b/internal/cli/remove_cmd.go index 9d81afc..1254353 100644 --- a/internal/cli/remove_cmd.go +++ b/internal/cli/remove_cmd.go @@ -33,11 +33,12 @@ func runRemove(cmd *cobra.Command, args []string) error { return err } - if _, exists := allConfig[key]; !exists { + _, actualKey, exists := allConfig.Get(key) + if !exists { return NewUserError("tmux config item '" + key + "' not found") } - err = config.RemoveConfigFromFile(key, removeLocal, opts.Dry) + err = config.RemoveConfigFromFile(actualKey, removeLocal, opts.Dry) if err != nil { return err } diff --git a/internal/cli/show_cmd.go b/internal/cli/show_cmd.go index 48eaf04..25de699 100644 --- a/internal/cli/show_cmd.go +++ b/internal/cli/show_cmd.go @@ -49,12 +49,12 @@ func runShow(cmd *cobra.Command, args []string) error { key = selected } - item, exists := allConfig[key] + item, actualKey, exists := allConfig.Get(key) if !exists { return NewUserError("tmux config item '" + key + "' not found") } - parsed := config.ParseConfig(key, item) + parsed := config.ParseConfig(actualKey, item) if showJSON { data, err := json.Marshal(parsed) diff --git a/internal/config/types.go b/internal/config/types.go index 9beec20..0ecc158 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -1,6 +1,8 @@ package config import ( + "strings" + "gopkg.in/yaml.v3" ) @@ -15,6 +17,23 @@ type GlobalConfig struct { // ConfigFile represents the top-level config file: map of session name -> config type ConfigFile map[string]TmuxConfigItemInput +// Get performs a case-insensitive lookup of a key in the config file. +// It returns the config item, the actual key as stored in the config, and whether it was found. +func (c ConfigFile) Get(key string) (TmuxConfigItemInput, string, bool) { + // Try exact match first + if item, ok := c[key]; ok { + return item, key, true + } + // Fall back to case-insensitive match + lower := strings.ToLower(key) + for k, v := range c { + if strings.ToLower(k) == lower { + return v, k, true + } + } + return TmuxConfigItemInput{}, "", false +} + // TmuxConfigItemInput represents a single tmux session configuration type TmuxConfigItemInput struct { Root string `yaml:"root"` diff --git a/internal/config/types_test.go b/internal/config/types_test.go index a987575..14579a2 100644 --- a/internal/config/types_test.go +++ b/internal/config/types_test.go @@ -180,6 +180,102 @@ another: } } +func TestConfigFile_Get_ExactMatch(t *testing.T) { + config := ConfigFile{ + "notes": {Root: "/tmp/notes"}, + "work": {Root: "/tmp/work"}, + } + + item, actualKey, ok := config.Get("notes") + if !ok { + t.Fatal("expected to find 'notes'") + } + if actualKey != "notes" { + t.Errorf("expected actualKey 'notes', got %q", actualKey) + } + if item.Root != "/tmp/notes" { + t.Errorf("expected Root '/tmp/notes', got %q", item.Root) + } +} + +func TestConfigFile_Get_CaseInsensitive(t *testing.T) { + config := ConfigFile{ + "Notes": {Root: "/tmp/notes"}, + "work": {Root: "/tmp/work"}, + } + + tests := []struct { + lookup string + wantKey string + wantFound bool + }{ + {"Notes", "Notes", true}, + {"notes", "Notes", true}, + {"NOTES", "Notes", true}, + {"nOtEs", "Notes", true}, + {"work", "work", true}, + {"Work", "work", true}, + {"WORK", "work", true}, + {"missing", "", false}, + } + + for _, tt := range tests { + t.Run(tt.lookup, func(t *testing.T) { + _, actualKey, ok := config.Get(tt.lookup) + if ok != tt.wantFound { + t.Errorf("Get(%q): found=%v, want %v", tt.lookup, ok, tt.wantFound) + } + if ok && actualKey != tt.wantKey { + t.Errorf("Get(%q): actualKey=%q, want %q", tt.lookup, actualKey, tt.wantKey) + } + }) + } +} + +func TestConfigFile_Get_ExactMatchTakesPrecedence(t *testing.T) { + // If both "notes" and "Notes" exist, exact match should win + config := ConfigFile{ + "notes": {Root: "/tmp/lower"}, + "Notes": {Root: "/tmp/upper"}, + } + + _, actualKey, ok := config.Get("notes") + if !ok { + t.Fatal("expected to find 'notes'") + } + if actualKey != "notes" { + t.Errorf("expected exact match 'notes', got %q", actualKey) + } + + _, actualKey, ok = config.Get("Notes") + if !ok { + t.Fatal("expected to find 'Notes'") + } + if actualKey != "Notes" { + t.Errorf("expected exact match 'Notes', got %q", actualKey) + } +} + +func TestConfigFile_Get_NotFound(t *testing.T) { + config := ConfigFile{ + "notes": {Root: "/tmp/notes"}, + } + + _, _, ok := config.Get("missing") + if ok { + t.Error("expected not found for 'missing'") + } +} + +func TestConfigFile_Get_EmptyConfig(t *testing.T) { + config := ConfigFile{} + + _, _, ok := config.Get("anything") + if ok { + t.Error("expected not found in empty config") + } +} + func TestDefaultEmptyLayout(t *testing.T) { if DefaultEmptyLayout.Cwd != "." { t.Errorf("expected Cwd to be '.', got %q", DefaultEmptyLayout.Cwd) diff --git a/internal/config/writer.go b/internal/config/writer.go index c64cbd4..8321b51 100644 --- a/internal/config/writer.go +++ b/internal/config/writer.go @@ -37,7 +37,7 @@ func AddSimpleConfigToFile(config ParsedTmuxConfigItem, local bool, dryRun bool) return err } - if _, exists := allConfigs[config.Name]; exists && !dryRun { + if _, _, exists := allConfigs.Get(config.Name); exists && !dryRun { return fmt.Errorf("%w: '%s'", ErrConfigItemExists, config.Name) }