1package config
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "log/slog"
9 "maps"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "runtime"
14 "slices"
15 "strconv"
16 "strings"
17 "testing"
18
19 "github.com/charmbracelet/catwalk/pkg/catwalk"
20 "github.com/charmbracelet/crush/internal/agent/hyper"
21 "github.com/charmbracelet/crush/internal/csync"
22 "github.com/charmbracelet/crush/internal/env"
23 "github.com/charmbracelet/crush/internal/fsext"
24 "github.com/charmbracelet/crush/internal/home"
25 "github.com/charmbracelet/crush/internal/log"
26 powernapConfig "github.com/charmbracelet/x/powernap/pkg/config"
27)
28
29const defaultCatwalkURL = "https://catwalk.charm.sh"
30
31// LoadReader config via io.Reader.
32func LoadReader(fd io.Reader) (*Config, error) {
33 data, err := io.ReadAll(fd)
34 if err != nil {
35 return nil, err
36 }
37
38 var config Config
39 err = json.Unmarshal(data, &config)
40 if err != nil {
41 return nil, err
42 }
43 return &config, err
44}
45
46// Load loads the configuration from the default paths.
47func Load(workingDir, dataDir string, debug bool) (*Config, error) {
48 configPaths := lookupConfigs(workingDir)
49
50 cfg, err := loadFromConfigPaths(configPaths)
51 if err != nil {
52 return nil, fmt.Errorf("failed to load config from paths %v: %w", configPaths, err)
53 }
54
55 cfg.dataConfigDir = GlobalConfigData()
56
57 cfg.setDefaults(workingDir, dataDir)
58
59 if debug {
60 cfg.Options.Debug = true
61 }
62
63 // Setup logs
64 log.Setup(
65 filepath.Join(cfg.Options.DataDirectory, "logs", fmt.Sprintf("%s.log", appName)),
66 cfg.Options.Debug,
67 )
68
69 if !isInsideWorktree() {
70 const depth = 2
71 const items = 100
72 slog.Warn("No git repository detected in working directory, will limit file walk operations", "depth", depth, "items", items)
73 assignIfNil(&cfg.Tools.Ls.MaxDepth, depth)
74 assignIfNil(&cfg.Tools.Ls.MaxItems, items)
75 assignIfNil(&cfg.Options.TUI.Completions.MaxDepth, depth)
76 assignIfNil(&cfg.Options.TUI.Completions.MaxItems, items)
77 }
78
79 // Load known providers, this loads the config from catwalk
80 providers, err := Providers(cfg)
81 if err != nil {
82 return nil, err
83 }
84 cfg.knownProviders = providers
85
86 env := env.New()
87 // Configure providers
88 valueResolver := NewShellVariableResolver(env)
89 cfg.resolver = valueResolver
90 if err := cfg.configureProviders(env, valueResolver, cfg.knownProviders); err != nil {
91 return nil, fmt.Errorf("failed to configure providers: %w", err)
92 }
93
94 if !cfg.IsConfigured() {
95 slog.Warn("No providers configured")
96 return cfg, nil
97 }
98
99 if err := cfg.configureSelectedModels(cfg.knownProviders); err != nil {
100 return nil, fmt.Errorf("failed to configure selected models: %w", err)
101 }
102 cfg.SetupAgents()
103 return cfg, nil
104}
105
106func PushPopCrushEnv() func() {
107 found := []string{}
108 for _, ev := range os.Environ() {
109 if strings.HasPrefix(ev, "CRUSH_") {
110 pair := strings.SplitN(ev, "=", 2)
111 if len(pair) != 2 {
112 continue
113 }
114 found = append(found, strings.TrimPrefix(pair[0], "CRUSH_"))
115 }
116 }
117 backups := make(map[string]string)
118 for _, ev := range found {
119 backups[ev] = os.Getenv(ev)
120 }
121
122 for _, ev := range found {
123 os.Setenv(ev, os.Getenv("CRUSH_"+ev))
124 }
125
126 restore := func() {
127 for k, v := range backups {
128 os.Setenv(k, v)
129 }
130 }
131 return restore
132}
133
134func (c *Config) configureProviders(env env.Env, resolver VariableResolver, knownProviders []catwalk.Provider) error {
135 c.importCopilot()
136
137 knownProviderNames := make(map[string]bool)
138 restore := PushPopCrushEnv()
139 defer restore()
140
141 for _, p := range knownProviders {
142 knownProviderNames[string(p.ID)] = true
143 config, configExists := c.Providers.Get(string(p.ID))
144 // if the user configured a known provider we need to allow it to override a couple of parameters
145 if configExists {
146 if config.BaseURL != "" {
147 p.APIEndpoint = config.BaseURL
148 }
149 if config.APIKey != "" {
150 p.APIKey = config.APIKey
151 }
152 if len(config.Models) > 0 {
153 models := []catwalk.Model{}
154 seen := make(map[string]bool)
155
156 for _, model := range config.Models {
157 if seen[model.ID] {
158 continue
159 }
160 seen[model.ID] = true
161 if model.Name == "" {
162 model.Name = model.ID
163 }
164 models = append(models, model)
165 }
166 for _, model := range p.Models {
167 if seen[model.ID] {
168 continue
169 }
170 seen[model.ID] = true
171 if model.Name == "" {
172 model.Name = model.ID
173 }
174 models = append(models, model)
175 }
176
177 p.Models = models
178 }
179 }
180
181 headers := map[string]string{}
182 if len(p.DefaultHeaders) > 0 {
183 maps.Copy(headers, p.DefaultHeaders)
184 }
185 if len(config.ExtraHeaders) > 0 {
186 maps.Copy(headers, config.ExtraHeaders)
187 }
188 prepared := ProviderConfig{
189 ID: string(p.ID),
190 Name: p.Name,
191 BaseURL: p.APIEndpoint,
192 APIKey: p.APIKey,
193 APIKeyTemplate: p.APIKey, // Store original template for re-resolution
194 OAuthToken: config.OAuthToken,
195 Type: p.Type,
196 Disable: config.Disable,
197 SystemPromptPrefix: config.SystemPromptPrefix,
198 ExtraHeaders: headers,
199 ExtraBody: config.ExtraBody,
200 ExtraParams: make(map[string]string),
201 Models: p.Models,
202 }
203
204 switch {
205 case p.ID == catwalk.InferenceProviderAnthropic && config.OAuthToken != nil:
206 prepared.SetupClaudeCode()
207 case p.ID == catwalk.InferenceProviderCopilot:
208 if config.OAuthToken != nil {
209 if token, ok := c.importCopilot(); ok {
210 prepared.OAuthToken = token
211 }
212 }
213 if config.OAuthToken != nil {
214 prepared.SetupGitHubCopilot()
215 }
216 }
217
218 switch p.ID {
219 // Handle specific providers that require additional configuration
220 case catwalk.InferenceProviderVertexAI:
221 if !hasVertexCredentials(env) {
222 if configExists {
223 slog.Warn("Skipping Vertex AI provider due to missing credentials")
224 c.Providers.Del(string(p.ID))
225 }
226 continue
227 }
228 prepared.ExtraParams["project"] = env.Get("VERTEXAI_PROJECT")
229 prepared.ExtraParams["location"] = env.Get("VERTEXAI_LOCATION")
230 case catwalk.InferenceProviderAzure:
231 endpoint, err := resolver.ResolveValue(p.APIEndpoint)
232 if err != nil || endpoint == "" {
233 if configExists {
234 slog.Warn("Skipping Azure provider due to missing API endpoint", "provider", p.ID, "error", err)
235 c.Providers.Del(string(p.ID))
236 }
237 continue
238 }
239 prepared.BaseURL = endpoint
240 prepared.ExtraParams["apiVersion"] = env.Get("AZURE_OPENAI_API_VERSION")
241 case catwalk.InferenceProviderBedrock:
242 if !hasAWSCredentials(env) {
243 if configExists {
244 slog.Warn("Skipping Bedrock provider due to missing AWS credentials")
245 c.Providers.Del(string(p.ID))
246 }
247 continue
248 }
249 prepared.ExtraParams["region"] = env.Get("AWS_REGION")
250 if prepared.ExtraParams["region"] == "" {
251 prepared.ExtraParams["region"] = env.Get("AWS_DEFAULT_REGION")
252 }
253 for _, model := range p.Models {
254 if !strings.HasPrefix(model.ID, "anthropic.") {
255 return fmt.Errorf("bedrock provider only supports anthropic models for now, found: %s", model.ID)
256 }
257 }
258 default:
259 // if the provider api or endpoint are missing we skip them
260 v, err := resolver.ResolveValue(p.APIKey)
261 if v == "" || err != nil {
262 if configExists {
263 slog.Warn("Skipping provider due to missing API key", "provider", p.ID)
264 c.Providers.Del(string(p.ID))
265 }
266 continue
267 }
268 }
269 c.Providers.Set(string(p.ID), prepared)
270 }
271
272 // validate the custom providers
273 for id, providerConfig := range c.Providers.Seq2() {
274 if knownProviderNames[id] {
275 continue
276 }
277
278 // Make sure the provider ID is set
279 providerConfig.ID = id
280 if providerConfig.Name == "" {
281 providerConfig.Name = id // Use ID as name if not set
282 }
283 // default to OpenAI if not set
284 if providerConfig.Type == "" {
285 providerConfig.Type = catwalk.TypeOpenAICompat
286 }
287 if !slices.Contains(catwalk.KnownProviderTypes(), providerConfig.Type) && providerConfig.Type != hyper.Name {
288 slog.Warn("Skipping custom provider due to unsupported provider type", "provider", id)
289 c.Providers.Del(id)
290 continue
291 }
292
293 if providerConfig.Disable {
294 slog.Debug("Skipping custom provider due to disable flag", "provider", id)
295 c.Providers.Del(id)
296 continue
297 }
298 if providerConfig.APIKey == "" {
299 slog.Warn("Provider is missing API key, this might be OK for local providers", "provider", id)
300 }
301 if providerConfig.BaseURL == "" {
302 slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id)
303 c.Providers.Del(id)
304 continue
305 }
306 if len(providerConfig.Models) == 0 {
307 slog.Warn("Skipping custom provider because the provider has no models", "provider", id)
308 c.Providers.Del(id)
309 continue
310 }
311 apiKey, err := resolver.ResolveValue(providerConfig.APIKey)
312 if apiKey == "" || err != nil {
313 slog.Warn("Provider is missing API key, this might be OK for local providers", "provider", id)
314 }
315 baseURL, err := resolver.ResolveValue(providerConfig.BaseURL)
316 if baseURL == "" || err != nil {
317 slog.Warn("Skipping custom provider due to missing API endpoint", "provider", id, "error", err)
318 c.Providers.Del(id)
319 continue
320 }
321
322 c.Providers.Set(id, providerConfig)
323 }
324 return nil
325}
326
327func (c *Config) setDefaults(workingDir, dataDir string) {
328 c.workingDir = workingDir
329 if c.Options == nil {
330 c.Options = &Options{}
331 }
332 if c.Options.TUI == nil {
333 c.Options.TUI = &TUIOptions{}
334 }
335 if c.Options.ContextPaths == nil {
336 c.Options.ContextPaths = []string{}
337 }
338 if dataDir != "" {
339 c.Options.DataDirectory = dataDir
340 } else if c.Options.DataDirectory == "" {
341 if path, ok := fsext.LookupClosest(workingDir, defaultDataDirectory); ok {
342 c.Options.DataDirectory = path
343 } else {
344 c.Options.DataDirectory = filepath.Join(workingDir, defaultDataDirectory)
345 }
346 }
347 if c.Providers == nil {
348 c.Providers = csync.NewMap[string, ProviderConfig]()
349 }
350 if c.Models == nil {
351 c.Models = make(map[SelectedModelType]SelectedModel)
352 }
353 if c.RecentModels == nil {
354 c.RecentModels = make(map[SelectedModelType][]SelectedModel)
355 }
356 if c.MCP == nil {
357 c.MCP = make(map[string]MCPConfig)
358 }
359 if c.LSP == nil {
360 c.LSP = make(map[string]LSPConfig)
361 }
362
363 // Apply defaults to LSP configurations
364 c.applyLSPDefaults()
365
366 // Add the default context paths if they are not already present
367 c.Options.ContextPaths = append(defaultContextPaths, c.Options.ContextPaths...)
368 slices.Sort(c.Options.ContextPaths)
369 c.Options.ContextPaths = slices.Compact(c.Options.ContextPaths)
370
371 if str, ok := os.LookupEnv("CRUSH_DISABLE_PROVIDER_AUTO_UPDATE"); ok {
372 c.Options.DisableProviderAutoUpdate, _ = strconv.ParseBool(str)
373 }
374
375 if c.Options.Attribution == nil {
376 c.Options.Attribution = &Attribution{
377 TrailerStyle: TrailerStyleAssistedBy,
378 GeneratedWith: true,
379 }
380 } else if c.Options.Attribution.TrailerStyle == "" {
381 // Migrate deprecated co_authored_by or apply default
382 if c.Options.Attribution.CoAuthoredBy != nil {
383 if *c.Options.Attribution.CoAuthoredBy {
384 c.Options.Attribution.TrailerStyle = TrailerStyleCoAuthoredBy
385 } else {
386 c.Options.Attribution.TrailerStyle = TrailerStyleNone
387 }
388 } else {
389 c.Options.Attribution.TrailerStyle = TrailerStyleAssistedBy
390 }
391 }
392 if c.Options.InitializeAs == "" {
393 c.Options.InitializeAs = defaultInitializeAs
394 }
395}
396
397// applyLSPDefaults applies default values from powernap to LSP configurations
398func (c *Config) applyLSPDefaults() {
399 // Get powernap's default configuration
400 configManager := powernapConfig.NewManager()
401 configManager.LoadDefaults()
402
403 // Apply defaults to each LSP configuration
404 for name, cfg := range c.LSP {
405 // Try to get defaults from powernap based on name or command name.
406 base, ok := configManager.GetServer(name)
407 if !ok {
408 base, ok = configManager.GetServer(cfg.Command)
409 if !ok {
410 continue
411 }
412 }
413 if cfg.Options == nil {
414 cfg.Options = base.Settings
415 }
416 if cfg.InitOptions == nil {
417 cfg.InitOptions = base.InitOptions
418 }
419 if len(cfg.FileTypes) == 0 {
420 cfg.FileTypes = base.FileTypes
421 }
422 if len(cfg.RootMarkers) == 0 {
423 cfg.RootMarkers = base.RootMarkers
424 }
425 if cfg.Command == "" {
426 cfg.Command = base.Command
427 }
428 if len(cfg.Args) == 0 {
429 cfg.Args = base.Args
430 }
431 if len(cfg.Env) == 0 {
432 cfg.Env = base.Environment
433 }
434 // Update the config in the map
435 c.LSP[name] = cfg
436 }
437}
438
439func (c *Config) defaultModelSelection(knownProviders []catwalk.Provider) (largeModel SelectedModel, smallModel SelectedModel, err error) {
440 if len(knownProviders) == 0 && c.Providers.Len() == 0 {
441 err = fmt.Errorf("no providers configured, please configure at least one provider")
442 return largeModel, smallModel, err
443 }
444
445 // Use the first provider enabled based on the known providers order
446 // if no provider found that is known use the first provider configured
447 for _, p := range knownProviders {
448 providerConfig, ok := c.Providers.Get(string(p.ID))
449 if !ok || providerConfig.Disable {
450 continue
451 }
452 defaultLargeModel := c.GetModel(string(p.ID), p.DefaultLargeModelID)
453 if defaultLargeModel == nil {
454 err = fmt.Errorf("default large model %s not found for provider %s", p.DefaultLargeModelID, p.ID)
455 return largeModel, smallModel, err
456 }
457 largeModel = SelectedModel{
458 Provider: string(p.ID),
459 Model: defaultLargeModel.ID,
460 MaxTokens: defaultLargeModel.DefaultMaxTokens,
461 ReasoningEffort: defaultLargeModel.DefaultReasoningEffort,
462 }
463
464 defaultSmallModel := c.GetModel(string(p.ID), p.DefaultSmallModelID)
465 if defaultSmallModel == nil {
466 err = fmt.Errorf("default small model %s not found for provider %s", p.DefaultSmallModelID, p.ID)
467 return largeModel, smallModel, err
468 }
469 smallModel = SelectedModel{
470 Provider: string(p.ID),
471 Model: defaultSmallModel.ID,
472 MaxTokens: defaultSmallModel.DefaultMaxTokens,
473 ReasoningEffort: defaultSmallModel.DefaultReasoningEffort,
474 }
475 return largeModel, smallModel, err
476 }
477
478 enabledProviders := c.EnabledProviders()
479 slices.SortFunc(enabledProviders, func(a, b ProviderConfig) int {
480 return strings.Compare(a.ID, b.ID)
481 })
482
483 if len(enabledProviders) == 0 {
484 err = fmt.Errorf("no providers configured, please configure at least one provider")
485 return largeModel, smallModel, err
486 }
487
488 providerConfig := enabledProviders[0]
489 if len(providerConfig.Models) == 0 {
490 err = fmt.Errorf("provider %s has no models configured", providerConfig.ID)
491 return largeModel, smallModel, err
492 }
493 defaultLargeModel := c.GetModel(providerConfig.ID, providerConfig.Models[0].ID)
494 largeModel = SelectedModel{
495 Provider: providerConfig.ID,
496 Model: defaultLargeModel.ID,
497 MaxTokens: defaultLargeModel.DefaultMaxTokens,
498 }
499 defaultSmallModel := c.GetModel(providerConfig.ID, providerConfig.Models[0].ID)
500 smallModel = SelectedModel{
501 Provider: providerConfig.ID,
502 Model: defaultSmallModel.ID,
503 MaxTokens: defaultSmallModel.DefaultMaxTokens,
504 }
505 return largeModel, smallModel, err
506}
507
508func (c *Config) configureSelectedModels(knownProviders []catwalk.Provider) error {
509 defaultLarge, defaultSmall, err := c.defaultModelSelection(knownProviders)
510 if err != nil {
511 return fmt.Errorf("failed to select default models: %w", err)
512 }
513 large, small := defaultLarge, defaultSmall
514
515 largeModelSelected, largeModelConfigured := c.Models[SelectedModelTypeLarge]
516 if largeModelConfigured {
517 if largeModelSelected.Model != "" {
518 large.Model = largeModelSelected.Model
519 }
520 if largeModelSelected.Provider != "" {
521 large.Provider = largeModelSelected.Provider
522 }
523 model := c.GetModel(large.Provider, large.Model)
524 if model == nil {
525 large = defaultLarge
526 // override the model type to large
527 err := c.UpdatePreferredModel(SelectedModelTypeLarge, large)
528 if err != nil {
529 return fmt.Errorf("failed to update preferred large model: %w", err)
530 }
531 } else {
532 if largeModelSelected.MaxTokens > 0 {
533 large.MaxTokens = largeModelSelected.MaxTokens
534 } else {
535 large.MaxTokens = model.DefaultMaxTokens
536 }
537 if largeModelSelected.ReasoningEffort != "" {
538 large.ReasoningEffort = largeModelSelected.ReasoningEffort
539 }
540 large.Think = largeModelSelected.Think
541 if largeModelSelected.Temperature != nil {
542 large.Temperature = largeModelSelected.Temperature
543 }
544 if largeModelSelected.TopP != nil {
545 large.TopP = largeModelSelected.TopP
546 }
547 if largeModelSelected.TopK != nil {
548 large.TopK = largeModelSelected.TopK
549 }
550 if largeModelSelected.FrequencyPenalty != nil {
551 large.FrequencyPenalty = largeModelSelected.FrequencyPenalty
552 }
553 if largeModelSelected.PresencePenalty != nil {
554 large.PresencePenalty = largeModelSelected.PresencePenalty
555 }
556 }
557 }
558 smallModelSelected, smallModelConfigured := c.Models[SelectedModelTypeSmall]
559 if smallModelConfigured {
560 if smallModelSelected.Model != "" {
561 small.Model = smallModelSelected.Model
562 }
563 if smallModelSelected.Provider != "" {
564 small.Provider = smallModelSelected.Provider
565 }
566
567 model := c.GetModel(small.Provider, small.Model)
568 if model == nil {
569 small = defaultSmall
570 // override the model type to small
571 err := c.UpdatePreferredModel(SelectedModelTypeSmall, small)
572 if err != nil {
573 return fmt.Errorf("failed to update preferred small model: %w", err)
574 }
575 } else {
576 if smallModelSelected.MaxTokens > 0 {
577 small.MaxTokens = smallModelSelected.MaxTokens
578 } else {
579 small.MaxTokens = model.DefaultMaxTokens
580 }
581 if smallModelSelected.ReasoningEffort != "" {
582 small.ReasoningEffort = smallModelSelected.ReasoningEffort
583 }
584 if smallModelSelected.Temperature != nil {
585 small.Temperature = smallModelSelected.Temperature
586 }
587 if smallModelSelected.TopP != nil {
588 small.TopP = smallModelSelected.TopP
589 }
590 if smallModelSelected.TopK != nil {
591 small.TopK = smallModelSelected.TopK
592 }
593 if smallModelSelected.FrequencyPenalty != nil {
594 small.FrequencyPenalty = smallModelSelected.FrequencyPenalty
595 }
596 if smallModelSelected.PresencePenalty != nil {
597 small.PresencePenalty = smallModelSelected.PresencePenalty
598 }
599 small.Think = smallModelSelected.Think
600 }
601 }
602 c.Models[SelectedModelTypeLarge] = large
603 c.Models[SelectedModelTypeSmall] = small
604 return nil
605}
606
607// lookupConfigs searches config files recursively from CWD up to FS root
608func lookupConfigs(cwd string) []string {
609 // prepend default config paths
610 configPaths := []string{
611 GlobalConfig(),
612 GlobalConfigData(),
613 }
614
615 configNames := []string{appName + ".json", "." + appName + ".json"}
616
617 foundConfigs, err := fsext.Lookup(cwd, configNames...)
618 if err != nil {
619 // returns at least default configs
620 return configPaths
621 }
622
623 // reverse order so last config has more priority
624 slices.Reverse(foundConfigs)
625
626 return append(configPaths, foundConfigs...)
627}
628
629func loadFromConfigPaths(configPaths []string) (*Config, error) {
630 var configs []io.Reader
631
632 for _, path := range configPaths {
633 fd, err := os.Open(path)
634 if err != nil {
635 if os.IsNotExist(err) {
636 continue
637 }
638 return nil, fmt.Errorf("failed to open config file %s: %w", path, err)
639 }
640 defer fd.Close()
641
642 configs = append(configs, fd)
643 }
644
645 return loadFromReaders(configs)
646}
647
648func loadFromReaders(readers []io.Reader) (*Config, error) {
649 if len(readers) == 0 {
650 return &Config{}, nil
651 }
652
653 merged, err := Merge(readers)
654 if err != nil {
655 return nil, fmt.Errorf("failed to merge configuration readers: %w", err)
656 }
657
658 return LoadReader(merged)
659}
660
661func hasVertexCredentials(env env.Env) bool {
662 hasProject := env.Get("VERTEXAI_PROJECT") != ""
663 hasLocation := env.Get("VERTEXAI_LOCATION") != ""
664 return hasProject && hasLocation
665}
666
667func hasAWSCredentials(env env.Env) bool {
668 if env.Get("AWS_BEARER_TOKEN_BEDROCK") != "" {
669 return true
670 }
671
672 if env.Get("AWS_ACCESS_KEY_ID") != "" && env.Get("AWS_SECRET_ACCESS_KEY") != "" {
673 return true
674 }
675
676 if env.Get("AWS_PROFILE") != "" || env.Get("AWS_DEFAULT_PROFILE") != "" {
677 return true
678 }
679
680 if env.Get("AWS_REGION") != "" || env.Get("AWS_DEFAULT_REGION") != "" {
681 return true
682 }
683
684 if env.Get("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
685 env.Get("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
686 return true
687 }
688
689 if _, err := os.Stat(filepath.Join(home.Dir(), ".aws/credentials")); err == nil && !testing.Testing() {
690 return true
691 }
692
693 return false
694}
695
696// GlobalConfig returns the global configuration file path for the application.
697func GlobalConfig() string {
698 xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
699 if xdgConfigHome != "" {
700 return filepath.Join(xdgConfigHome, appName, fmt.Sprintf("%s.json", appName))
701 }
702
703 return filepath.Join(home.Dir(), ".config", appName, fmt.Sprintf("%s.json", appName))
704}
705
706// GlobalConfigData returns the path to the main data directory for the application.
707// this config is used when the app overrides configurations instead of updating the global config.
708func GlobalConfigData() string {
709 xdgDataHome := os.Getenv("XDG_DATA_HOME")
710 if xdgDataHome != "" {
711 return filepath.Join(xdgDataHome, appName, fmt.Sprintf("%s.json", appName))
712 }
713
714 // return the path to the main data directory
715 // for windows, it should be in `%LOCALAPPDATA%/crush/`
716 // for linux and macOS, it should be in `$HOME/.local/share/crush/`
717 if runtime.GOOS == "windows" {
718 localAppData := os.Getenv("LOCALAPPDATA")
719 if localAppData == "" {
720 localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
721 }
722 return filepath.Join(localAppData, appName, fmt.Sprintf("%s.json", appName))
723 }
724
725 return filepath.Join(home.Dir(), ".local", "share", appName, fmt.Sprintf("%s.json", appName))
726}
727
728func assignIfNil[T any](ptr **T, val T) {
729 if *ptr == nil {
730 *ptr = &val
731 }
732}
733
734func isInsideWorktree() bool {
735 bts, err := exec.CommandContext(
736 context.Background(),
737 "git", "rev-parse",
738 "--is-inside-work-tree",
739 ).CombinedOutput()
740 return err == nil && strings.TrimSpace(string(bts)) == "true"
741}