@@ -27,19 +27,21 @@ const (
2727// Config represents the per-user application configuration stored under
2828// %LOCALAPPDATA%\Pangolin\pangolin.json (or %APPDATA% as a fallback).
2929type Config struct {
30- DNSOverride * bool `json:"dnsOverride,omitempty"`
31- DNSTunnel * bool `json:"dnsTunnel,omitempty"`
32- PrimaryDNS * string `json:"primaryDNS,omitempty"`
33- SecondaryDNS * string `json:"secondaryDNS,omitempty"`
34- DefaultServerURL * string `json:"defaultServerURL,omitempty"`
35- UserSettingsDisabled * bool `json:"userSettingsDisabled,omitempty"`
36- AuthPath * string `json:"authPath,omitempty"`
30+ DNSOverride * bool `json:"dnsOverride,omitempty"`
31+ DNSTunnel * bool `json:"dnsTunnel,omitempty"`
32+ PrimaryDNS * string `json:"primaryDNS,omitempty"`
33+ SecondaryDNS * string `json:"secondaryDNS,omitempty"`
34+ DefaultServerURL * string `json:"defaultServerURL,omitempty"`
35+ UserSettingsDisabled * bool `json:"userSettingsDisabled,omitempty"`
36+ AuthPath * string `json:"authPath,omitempty"`
37+ OpenStatusTabOnConnect * bool `json:"openStatusTabOnConnect,omitempty"`
3738}
3839
3940// SystemConfig represents machine-wide configuration stored under
40- // %ProgramData%\Pangolin\pangolin.json. This is used by background
41- // services (manager, tunnel, etc.) and for global settings like log level.
41+ // %ProgramData%\Pangolin\pangolin.json. It supports the same settings as
42+ // per-user config plus system-only fields like log level.
4243type SystemConfig struct {
44+ Config
4345 LogLevel * string `json:"logLevel,omitempty"`
4446}
4547
@@ -92,26 +94,15 @@ func (cm *ConfigManager) GetConfigCopy() *Config {
9294// load loads the configuration from the file
9395// Returns a default config if the file doesn't exist or can't be read
9496func (cm * ConfigManager ) load () * Config {
95- // Check if file exists
96- if _ , err := os .Stat (cm .configPath ); os .IsNotExist (err ) {
97- return & Config {}
98- }
97+ // Load machine-wide defaults first, then overlay user-specific values.
98+ merged := configFromSystemConfig (LoadSystemConfig ())
9999
100- // Read file
101- data , err := os .ReadFile (cm .configPath )
102- if err != nil {
103- logger .Error ("Error loading config: %v" , err )
104- return & Config {}
100+ userCfg , ok := cm .loadUserConfig ()
101+ if ! ok {
102+ return merged
105103 }
106104
107- // Parse JSON
108- var config Config
109- if err := json .Unmarshal (data , & config ); err != nil {
110- logger .Error ("Error parsing config: %v" , err )
111- return & Config {}
112- }
113-
114- return & config
105+ return mergeConfig (merged , userCfg )
115106}
116107
117108// Load loads the configuration from the file
@@ -266,6 +257,18 @@ func (cm *ConfigManager) GetAuthPath() string {
266257 return ""
267258}
268259
260+ // GetOpenStatusTabOnConnect returns whether the tray Connect action should
261+ // open the preferences window directly on the Status tab after a successful connect.
262+ func (cm * ConfigManager ) GetOpenStatusTabOnConnect () bool {
263+ cm .mu .RLock ()
264+ defer cm .mu .RUnlock ()
265+
266+ if cm .config != nil && cm .config .OpenStatusTabOnConnect != nil {
267+ return * cm .config .OpenStatusTabOnConnect
268+ }
269+ return false
270+ }
271+
269272// SetAuthPath sets the auth path and saves to config
270273func (cm * ConfigManager ) SetAuthPath (value string ) bool {
271274 cm .mu .Lock ()
@@ -360,40 +363,120 @@ func GetSystemLogLevel() string {
360363// getConfigCopy creates a deep copy of the current config
361364// Caller must hold the lock
362365func (cm * ConfigManager ) getConfigCopy () * Config {
363- if cm .config == nil {
366+ return copyConfig (cm .config )
367+ }
368+
369+ // loadUserConfig loads the per-user config from disk.
370+ func (cm * ConfigManager ) loadUserConfig () (* Config , bool ) {
371+ if _ , err := os .Stat (cm .configPath ); os .IsNotExist (err ) {
372+ return nil , false
373+ }
374+
375+ data , err := os .ReadFile (cm .configPath )
376+ if err != nil {
377+ logger .Error ("Error loading config: %v" , err )
378+ return nil , false
379+ }
380+
381+ var cfg Config
382+ if err := json .Unmarshal (data , & cfg ); err != nil {
383+ logger .Error ("Error parsing config: %v" , err )
384+ return nil , false
385+ }
386+
387+ return & cfg , true
388+ }
389+
390+ // configFromSystemConfig extracts shared config fields from system config.
391+ func configFromSystemConfig (sys * SystemConfig ) * Config {
392+ if sys == nil {
393+ return & Config {}
394+ }
395+ return copyConfig (& sys .Config )
396+ }
397+
398+ // mergeConfig overlays override values onto base values.
399+ func mergeConfig (base , override * Config ) * Config {
400+ merged := copyConfig (base )
401+ if override == nil {
402+ return merged
403+ }
404+
405+ if override .DNSOverride != nil {
406+ v := * override .DNSOverride
407+ merged .DNSOverride = & v
408+ }
409+ if override .DNSTunnel != nil {
410+ v := * override .DNSTunnel
411+ merged .DNSTunnel = & v
412+ }
413+ if override .PrimaryDNS != nil {
414+ v := * override .PrimaryDNS
415+ merged .PrimaryDNS = & v
416+ }
417+ if override .SecondaryDNS != nil {
418+ v := * override .SecondaryDNS
419+ merged .SecondaryDNS = & v
420+ }
421+ if override .DefaultServerURL != nil {
422+ v := * override .DefaultServerURL
423+ merged .DefaultServerURL = & v
424+ }
425+ if override .UserSettingsDisabled != nil {
426+ v := * override .UserSettingsDisabled
427+ merged .UserSettingsDisabled = & v
428+ }
429+ if override .AuthPath != nil {
430+ v := * override .AuthPath
431+ merged .AuthPath = & v
432+ }
433+ if override .OpenStatusTabOnConnect != nil {
434+ v := * override .OpenStatusTabOnConnect
435+ merged .OpenStatusTabOnConnect = & v
436+ }
437+
438+ return merged
439+ }
440+
441+ // copyConfig creates a deep copy of all pointer fields.
442+ func copyConfig (src * Config ) * Config {
443+ if src == nil {
364444 return & Config {}
365445 }
366446
367- // Create a new config and copy all pointer fields
368447 cfg := & Config {}
369- if cm . config .DNSOverride != nil {
370- dnsOverride := * cm . config .DNSOverride
448+ if src .DNSOverride != nil {
449+ dnsOverride := * src .DNSOverride
371450 cfg .DNSOverride = & dnsOverride
372451 }
373- if cm . config .DNSTunnel != nil {
374- dnsTunnel := * cm . config .DNSTunnel
452+ if src .DNSTunnel != nil {
453+ dnsTunnel := * src .DNSTunnel
375454 cfg .DNSTunnel = & dnsTunnel
376455 }
377- if cm . config .PrimaryDNS != nil {
378- primaryDNS := * cm . config .PrimaryDNS
456+ if src .PrimaryDNS != nil {
457+ primaryDNS := * src .PrimaryDNS
379458 cfg .PrimaryDNS = & primaryDNS
380459 }
381- if cm . config .SecondaryDNS != nil {
382- secondaryDNS := * cm . config .SecondaryDNS
460+ if src .SecondaryDNS != nil {
461+ secondaryDNS := * src .SecondaryDNS
383462 cfg .SecondaryDNS = & secondaryDNS
384463 }
385- if cm . config .DefaultServerURL != nil {
386- defaultServerURL := * cm . config .DefaultServerURL
464+ if src .DefaultServerURL != nil {
465+ defaultServerURL := * src .DefaultServerURL
387466 cfg .DefaultServerURL = & defaultServerURL
388467 }
389- if cm . config .UserSettingsDisabled != nil {
390- userSettingsDisabled := * cm . config .UserSettingsDisabled
468+ if src .UserSettingsDisabled != nil {
469+ userSettingsDisabled := * src .UserSettingsDisabled
391470 cfg .UserSettingsDisabled = & userSettingsDisabled
392471 }
393- if cm . config .AuthPath != nil {
394- authPath := * cm . config .AuthPath
472+ if src .AuthPath != nil {
473+ authPath := * src .AuthPath
395474 cfg .AuthPath = & authPath
396475 }
476+ if src .OpenStatusTabOnConnect != nil {
477+ openStatusTabOnConnect := * src .OpenStatusTabOnConnect
478+ cfg .OpenStatusTabOnConnect = & openStatusTabOnConnect
479+ }
397480 return cfg
398481}
399482
0 commit comments