Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions cmd/ghostty-switch/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,32 @@ func TestCLIProfileApply(t *testing.T) {
}
}

func TestCLIProfileApplyClearsListValuedKeys(t *testing.T) {
bin := buildBinary(t)
dir := t.TempDir()
overridePath := filepath.Join(dir, "overrides")
configPath := writeTestConfig(t, dir, overridePath)

cmd := exec.Command(bin, "--config", configPath, "profile", "dark")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("profile dark: %v\n%s", err, out)
}

data, err := os.ReadFile(overridePath)
if err != nil {
t.Fatal(err)
}

// The dark profile sets font-family, which Ghostty treats as list-valued.
// The override file must emit a clearing line before the value line so the
// override replaces — rather than appends to — the main config's font-family.
wantPair := "font-family =\nfont-family = JetBrains Mono\n"
if !strings.Contains(string(data), wantPair) {
t.Errorf("override file missing clearing+value pair %q, got:\n%s", wantPair, data)
}
}

func TestCLIConfigFlagWithoutValue(t *testing.T) {
bin := buildBinary(t)
cmd := exec.Command(bin, "--config")
Expand Down
16 changes: 16 additions & 0 deletions internal/overrides/overrides.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ import (

const header = "# Managed by ghostty-switch — do not edit manually"

// listValuedKeys are Ghostty config directives that accumulate across
// occurrences (each line appends to a list) rather than scalar last-write-wins.
// Each is preceded in the override file by a clearing line ("key =") so the
// override replaces — rather than appends to — values from the main config
// that loaded earlier. See issue #20.
var listValuedKeys = map[string]struct{}{
"font-family": {},
"font-family-bold": {},
"font-family-italic": {},
"font-family-bold-italic": {},
"keybind": {},
}

// Read parses the override file into a key-value map. Returns an empty map
// if the file does not exist.
func Read(path string) (map[string]string, error) {
Expand Down Expand Up @@ -59,6 +72,9 @@ func Write(path string, settings map[string]string) error {

keys := sortedKeys(settings)
for _, k := range keys {
if _, isList := listValuedKeys[k]; isList {
fmt.Fprintf(&b, "%s =\n", k)
}
fmt.Fprintf(&b, "%s = %s\n", k, settings[k])
}

Expand Down
90 changes: 90 additions & 0 deletions internal/overrides/overrides_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,96 @@ func TestReadSkipsCommentsAndBlanks(t *testing.T) {
}
}

func TestWriteEmitsClearingLineForListValuedKeys(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "overrides")

settings := map[string]string{
"font-family": "MartianMono Nerd Font",
"font-size": "14",
"theme": "Nord",
}

if err := Write(path, settings); err != nil {
t.Fatalf("Write: %v", err)
}

data, _ := os.ReadFile(path)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred to #22 — the same data, _ := os.ReadFile(path) pattern exists in TestWriteIncludesHeader:67 and TestWriteSortsKeys:88 already, so the cleanup belongs in a scoped project-wide pass rather than bundled into this PR.

content := string(data)

// Clearing line must precede the value line, consecutive, for list-valued keys.
wantPair := "font-family =\nfont-family = MartianMono Nerd Font\n"
if !strings.Contains(content, wantPair) {
t.Errorf("expected output to contain clearing+value pair %q, got:\n%s", wantPair, content)
}

// Scalar keys must not get a clearing line.
for _, scalar := range []string{"font-size", "theme"} {
bare := scalar + " =\n"
if strings.Contains(content, bare) {
t.Errorf("scalar key %q must not have a clearing line; got:\n%s", scalar, content)
}
}
}

func TestWriteEmitsClearingLineForEachListValuedKey(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "overrides")

settings := map[string]string{
"font-family": "Mono",
"font-family-bold": "Mono Bold",
"font-family-italic": "Mono Italic",
"font-family-bold-italic": "Mono Bold Italic",
"keybind": "ctrl+a=copy_to_clipboard",
}

if err := Write(path, settings); err != nil {
t.Fatalf("Write: %v", err)
}

content := func() string {
data, _ := os.ReadFile(path)
return string(data)
Comment on lines +171 to +172

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred to #22 — same as above; tracked there for a project-wide pass across the four current instances in overrides_test.go.

}()

for k, v := range settings {
want := k + " =\n" + k + " = " + v + "\n"
if !strings.Contains(content, want) {
t.Errorf("expected clearing+value pair for %q (%q), got:\n%s", k, want, content)
}
}
}

func TestReadWriteRoundTripPreservesListValuedKeys(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "overrides")

original := map[string]string{
"font-family": "MartianMono Nerd Font",
"keybind": "ctrl+a=copy_to_clipboard",
"theme": "Nord",
}

if err := Write(path, original); err != nil {
t.Fatalf("Write: %v", err)
}

got, err := Read(path)
if err != nil {
t.Fatalf("Read: %v", err)
}

if len(got) != len(original) {
t.Errorf("expected %d entries, got %d: %v", len(original), len(got), got)
}
for k, v := range original {
if got[k] != v {
t.Errorf("key %q = %q, want %q", k, got[k], v)
}
}
}

func TestMerge(t *testing.T) {
existing := map[string]string{
"theme": "Nord",
Expand Down
Loading