11package model
22
3- import "context"
3+ import (
4+ "bufio"
5+ "context"
6+ "fmt"
7+ "os"
8+ "path/filepath"
9+ "strings"
10+
11+ "github.com/sirupsen/logrus"
12+ )
413
514// GhosttyApp handles Ghostty terminal configuration files
615type GhosttyApp struct {
@@ -18,11 +27,188 @@ func (g *GhosttyApp) Name() string {
1827func (g * GhosttyApp ) GetConfigPaths () []string {
1928 return []string {
2029 "~/.config/ghostty/config" ,
21- "~/.config/ghostty" ,
2230 }
2331}
2432
2533func (g * GhosttyApp ) CollectDotfiles (ctx context.Context ) ([]DotfileItem , error ) {
2634 skipIgnored := true
2735 return g .CollectFromPaths (ctx , g .Name (), g .GetConfigPaths (), & skipIgnored )
28- }
36+ }
37+
38+ // configLine represents a line in the Ghostty config file
39+ type configLine struct {
40+ isComment bool
41+ isBlank bool
42+ key string
43+ value string
44+ raw string // for comments and blank lines
45+ }
46+
47+ // parseGhosttyConfig parses Ghostty config content into structured lines
48+ func (g * GhosttyApp ) parseGhosttyConfig (content string ) []configLine {
49+ var lines []configLine
50+ scanner := bufio .NewScanner (strings .NewReader (content ))
51+
52+ for scanner .Scan () {
53+ line := scanner .Text ()
54+ trimmed := strings .TrimSpace (line )
55+
56+ if trimmed == "" {
57+ lines = append (lines , configLine {
58+ isBlank : true ,
59+ raw : line ,
60+ })
61+ } else if strings .HasPrefix (trimmed , "#" ) {
62+ lines = append (lines , configLine {
63+ isComment : true ,
64+ raw : line ,
65+ })
66+ } else if strings .Contains (line , "=" ) {
67+ parts := strings .SplitN (line , "=" , 2 )
68+ key := strings .TrimSpace (parts [0 ])
69+ value := ""
70+ if len (parts ) > 1 {
71+ value = strings .TrimSpace (parts [1 ])
72+ }
73+ lines = append (lines , configLine {
74+ key : key ,
75+ value : value ,
76+ raw : line ,
77+ })
78+ } else {
79+ // Treat as a comment if it doesn't match key=value format
80+ lines = append (lines , configLine {
81+ isComment : true ,
82+ raw : line ,
83+ })
84+ }
85+ }
86+
87+ return lines
88+ }
89+
90+ // mergeGhosttyConfigs merges remote config with local config, local has priority
91+ func (g * GhosttyApp ) mergeGhosttyConfigs (localLines , remoteLines []configLine ) []configLine {
92+ // Create a map of local keys for quick lookup
93+ localKeys := make (map [string ]bool )
94+ for _ , line := range localLines {
95+ if ! line .isComment && ! line .isBlank && line .key != "" {
96+ localKeys [line .key ] = true
97+ }
98+ }
99+
100+ // Start with local config
101+ merged := make ([]configLine , len (localLines ))
102+ copy (merged , localLines )
103+
104+ // Add keys from remote that don't exist in local
105+ for _ , remoteLine := range remoteLines {
106+ if ! remoteLine .isComment && ! remoteLine .isBlank && remoteLine .key != "" {
107+ if ! localKeys [remoteLine .key ] {
108+ merged = append (merged , remoteLine )
109+ }
110+ }
111+ }
112+
113+ return merged
114+ }
115+
116+ // formatGhosttyConfig converts config lines back to string
117+ func (g * GhosttyApp ) formatGhosttyConfig (lines []configLine ) string {
118+ var result []string
119+ for _ , line := range lines {
120+ if line .isComment || line .isBlank {
121+ result = append (result , line .raw )
122+ } else {
123+ result = append (result , fmt .Sprintf ("%s = %s" , line .key , line .value ))
124+ }
125+ }
126+ return strings .Join (result , "\n " )
127+ }
128+
129+ // Save overrides the base Save method to handle Ghostty's specific config format
130+ func (g * GhosttyApp ) Save (ctx context.Context , files map [string ]string , isDryRun bool ) error {
131+ for path , remoteContent := range files {
132+ expandedPath , err := g .expandPath (path )
133+ if err != nil {
134+ logrus .Warnf ("Failed to expand path %s: %v" , path , err )
135+ continue
136+ }
137+
138+ // Read existing local content if file exists
139+ var localContent string
140+ if existingBytes , err := os .ReadFile (expandedPath ); err == nil {
141+ localContent = string (existingBytes )
142+ } else if ! os .IsNotExist (err ) {
143+ logrus .Warnf ("Failed to read existing file %s: %v" , expandedPath , err )
144+ continue
145+ }
146+
147+ // Parse both configs
148+ localLines := g .parseGhosttyConfig (localContent )
149+ remoteLines := g .parseGhosttyConfig (remoteContent )
150+
151+ // Merge configs (local has priority)
152+ mergedLines := g .mergeGhosttyConfigs (localLines , remoteLines )
153+ mergedContent := g .formatGhosttyConfig (mergedLines )
154+
155+ // Check if there are any differences
156+ if localContent == mergedContent {
157+ logrus .Infof ("No changes needed for %s" , expandedPath )
158+ continue
159+ }
160+
161+ if isDryRun {
162+ // In dry-run mode, show the diff
163+ fmt .Printf ("\n 📄 %s:\n " , expandedPath )
164+ fmt .Println ("--- Changes to be applied ---" )
165+
166+ // Show added keys (from remote)
167+ remoteKeys := make (map [string ]string )
168+ for _ , line := range remoteLines {
169+ if ! line .isComment && ! line .isBlank && line .key != "" {
170+ remoteKeys [line .key ] = line .value
171+ }
172+ }
173+
174+ localKeys := make (map [string ]string )
175+ for _ , line := range localLines {
176+ if ! line .isComment && ! line .isBlank && line .key != "" {
177+ localKeys [line .key ] = line .value
178+ }
179+ }
180+
181+ // Show new keys from remote
182+ hasChanges := false
183+ for key , value := range remoteKeys {
184+ if _ , exists := localKeys [key ]; ! exists {
185+ fmt .Printf ("+ %s = %s (from remote)\n " , key , value )
186+ hasChanges = true
187+ }
188+ }
189+
190+ if ! hasChanges {
191+ fmt .Println ("No new keys from remote" )
192+ }
193+
194+ fmt .Println ("--- End of changes ---" )
195+ continue
196+ }
197+
198+ // Ensure directory exists
199+ dir := filepath .Dir (expandedPath )
200+ if err := os .MkdirAll (dir , 0755 ); err != nil {
201+ logrus .Warnf ("Failed to create directory %s: %v" , dir , err )
202+ continue
203+ }
204+
205+ // Write merged content
206+ if err := os .WriteFile (expandedPath , []byte (mergedContent ), 0644 ); err != nil {
207+ logrus .Warnf ("Failed to save file %s: %v" , expandedPath , err )
208+ } else {
209+ logrus .Infof ("Saved merged config to %s" , expandedPath )
210+ }
211+ }
212+
213+ return nil
214+ }
0 commit comments