feat: config name case insensitivity

This commit is contained in:
2026-02-08 23:27:13 +02:00
parent f8e2de631a
commit 5c71d7bc11
8 changed files with 163 additions and 11 deletions

View File

@@ -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")

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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"`

View File

@@ -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)

View File

@@ -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)
}