diff --git a/apps/finicky/src/config/vm.go b/apps/finicky/src/config/vm.go index 3383dd8d..e30c67ca 100644 --- a/apps/finicky/src/config/vm.go +++ b/apps/finicky/src/config/vm.go @@ -17,10 +17,11 @@ type VM struct { // ConfigOptions holds the values of all runtime config options. type ConfigOptions struct { - KeepRunning bool - HideIcon bool - LogRequests bool - CheckForUpdates bool + KeepRunning bool + HideIcon bool + HideWindowOnStart bool + LogRequests bool + CheckForUpdates bool } // ConfigState represents the current state of the configuration @@ -154,19 +155,21 @@ func (vm *VM) SetIsJSConfig(v bool) { // Safe to call on a nil VM — returns defaults in that case. func (vm *VM) GetAllConfigOptions() ConfigOptions { defaults := ConfigOptions{ - KeepRunning: true, - HideIcon: false, - LogRequests: false, - CheckForUpdates: true, + KeepRunning: true, + HideIcon: false, + HideWindowOnStart: false, + LogRequests: false, + CheckForUpdates: true, } if vm == nil || vm.runtime == nil { return defaults } script := `({ - keepRunning: finickyConfigAPI.getOption('keepRunning', finalConfig, true), - hideIcon: finickyConfigAPI.getOption('hideIcon', finalConfig, false), - logRequests: finickyConfigAPI.getOption('logRequests', finalConfig, false), - checkForUpdates: finickyConfigAPI.getOption('checkForUpdates', finalConfig, true) + keepRunning: finickyConfigAPI.getOption('keepRunning', finalConfig, true), + hideIcon: finickyConfigAPI.getOption('hideIcon', finalConfig, false), + hideWindowOnStart: finickyConfigAPI.getOption('hideWindowOnStart', finalConfig, false), + logRequests: finickyConfigAPI.getOption('logRequests', finalConfig, false), + checkForUpdates: finickyConfigAPI.getOption('checkForUpdates', finalConfig, true) })` val, err := vm.runtime.RunString(script) if err != nil { @@ -175,10 +178,11 @@ func (vm *VM) GetAllConfigOptions() ConfigOptions { } obj := val.ToObject(vm.runtime) return ConfigOptions{ - KeepRunning: obj.Get("keepRunning").ToBoolean(), - HideIcon: obj.Get("hideIcon").ToBoolean(), - LogRequests: obj.Get("logRequests").ToBoolean(), - CheckForUpdates: obj.Get("checkForUpdates").ToBoolean(), + KeepRunning: obj.Get("keepRunning").ToBoolean(), + HideIcon: obj.Get("hideIcon").ToBoolean(), + HideWindowOnStart: obj.Get("hideWindowOnStart").ToBoolean(), + LogRequests: obj.Get("logRequests").ToBoolean(), + CheckForUpdates: obj.Get("checkForUpdates").ToBoolean(), } } diff --git a/apps/finicky/src/main.go b/apps/finicky/src/main.go index 6297c9c0..76891173 100644 --- a/apps/finicky/src/main.go +++ b/apps/finicky/src/main.go @@ -249,10 +249,13 @@ func main() { }() shouldHideIcon := false + hideWindowOnStart := false if vm != nil { - shouldHideIcon = vm.GetAllConfigOptions().HideIcon + opts := vm.GetAllConfigOptions() + shouldHideIcon = opts.HideIcon + hideWindowOnStart = opts.HideWindowOnStart } - C.RunApp(C.bool(forceWindowOpen), C.bool(!shouldHideIcon), C.bool(shouldKeepRunning)) + C.RunApp(C.bool(forceWindowOpen), C.bool(!shouldHideIcon), C.bool(shouldKeepRunning), C.bool(hideWindowOnStart)) } func handleRuntimeError(err error) { @@ -446,7 +449,7 @@ func setupVM(cfw *config.ConfigFileWatcher, namespace string) (*config.VM, error slog.Warn("Failed to load rules file", "error", rulesErr) } else { resolver.SetCachedRules(rf) - if rf.DefaultBrowser != "" || len(rf.Rules) > 0 { + if rf.DefaultBrowser != "" || len(rf.Rules) > 0 || rf.Options != nil { script, scriptErr := rules.ToJSConfigScript(rf, namespace) if scriptErr != nil { return nil, fmt.Errorf("failed to generate config from rules: %v", scriptErr) @@ -484,10 +487,11 @@ func setupVM(cfw *config.ConfigFileWatcher, namespace string) (*config.VM, error "configPath": util.ShortenPath(configInfo.ConfigPath), "isJSConfig": newVM.IsJSConfig(), "options": map[string]interface{}{ - "keepRunning": opts.KeepRunning, - "hideIcon": opts.HideIcon, - "logRequests": opts.LogRequests, - "checkForUpdates": opts.CheckForUpdates, + "keepRunning": opts.KeepRunning, + "hideIcon": opts.HideIcon, + "hideWindowOnStart": opts.HideWindowOnStart, + "logRequests": opts.LogRequests, + "checkForUpdates": opts.CheckForUpdates, }, }) diff --git a/apps/finicky/src/main.h b/apps/finicky/src/main.h index e8029c47..ab074861 100644 --- a/apps/finicky/src/main.h +++ b/apps/finicky/src/main.h @@ -20,12 +20,13 @@ extern char* GetCurrentConfigPath(); @property (nonatomic) bool receivedURL; @property (nonatomic) bool keepRunning; @property (nonatomic) bool showMenuItem; - - (instancetype)initWithForceOpenWindow:(bool)forceOpenWindow initShow:(bool)showMenuItem keepRunning:(bool)keepRunning; + @property (nonatomic) bool hideWindowOnStart; + - (instancetype)initWithForceOpenWindow:(bool)forceOpenWindow initShow:(bool)showMenuItem keepRunning:(bool)keepRunning hideWindowOnStart:(bool)hideWindowOnStart; - (void)handleGetURLEvent:(NSAppleEventDescriptor *) event withReplyEvent:(NSAppleEventDescriptor *)replyEvent; - (bool)application:(NSApplication *)sender openFile:(NSString *)filename; @end #endif -void RunApp(bool forceOpenWindow, bool showStatusItem, bool keepRunning); +void RunApp(bool forceOpenWindow, bool showStatusItem, bool keepRunning, bool hideWindowOnStart); #endif /* MAIN_H */ diff --git a/apps/finicky/src/main.m b/apps/finicky/src/main.m index ad2aa539..5838c30d 100644 --- a/apps/finicky/src/main.m +++ b/apps/finicky/src/main.m @@ -15,12 +15,13 @@ - (void)showWindowAction:(id)sender; @implementation BrowseAppDelegate -- (instancetype)initWithForceOpenWindow:(bool)forceOpenWindow initShow:(bool)showMenuItem keepRunning:(bool)keepRunning { +- (instancetype)initWithForceOpenWindow:(bool)forceOpenWindow initShow:(bool)showMenuItem keepRunning:(bool)keepRunning hideWindowOnStart:(bool)hideWindowOnStart { self = [super init]; if (self) { _forceOpenWindow = forceOpenWindow; _showMenuItem = showMenuItem; _keepRunning = keepRunning; + _hideWindowOnStart = hideWindowOnStart; _receivedURL = false; } return self; @@ -32,8 +33,9 @@ - (void)applicationDidFinishLaunching:(NSNotification *)notification { bool openWindow = self.forceOpenWindow; if (!openWindow) { - // Even if we aren't forcing the window to open, we still want to open it if didn't receive a URL - openWindow = !self.receivedURL; + // Open the window on launch unless we received a URL to handle, or the + // user opted out of the automatic launch window via hideWindowOnStart. + openWindow = !self.receivedURL && !self.hideWindowOnStart; } // Only show menu item if the option is enabled, and we either didn't receive a URL or we are keeping @@ -259,12 +261,12 @@ - (void)application:(NSApplication *)application didFailToContinueUserActivityWi @end -void RunApp(bool forceOpenWindow, bool showStatusItem, bool keepRunning) { +void RunApp(bool forceOpenWindow, bool showStatusItem, bool keepRunning, bool hideWindowOnStart) { @autoreleasepool { // Initialize on the main thread directly, not async [NSApplication sharedApplication]; - BrowseAppDelegate *app = [[BrowseAppDelegate alloc] initWithForceOpenWindow:forceOpenWindow initShow:showStatusItem keepRunning:keepRunning]; + BrowseAppDelegate *app = [[BrowseAppDelegate alloc] initWithForceOpenWindow:forceOpenWindow initShow:showStatusItem keepRunning:keepRunning hideWindowOnStart:hideWindowOnStart]; [NSApp setDelegate:app]; [NSApp finishLaunching]; diff --git a/apps/finicky/src/rules/rules.go b/apps/finicky/src/rules/rules.go index 30909ec4..46a03a2c 100644 --- a/apps/finicky/src/rules/rules.go +++ b/apps/finicky/src/rules/rules.go @@ -53,10 +53,11 @@ func (r Rule) MarshalJSON() ([]byte, error) { } type Options struct { - KeepRunning *bool `json:"keepRunning,omitempty"` - HideIcon *bool `json:"hideIcon,omitempty"` - LogRequests *bool `json:"logRequests,omitempty"` - CheckForUpdates *bool `json:"checkForUpdates,omitempty"` + KeepRunning *bool `json:"keepRunning,omitempty"` + HideIcon *bool `json:"hideIcon,omitempty"` + HideWindowOnStart *bool `json:"hideWindowOnStart,omitempty"` + LogRequests *bool `json:"logRequests,omitempty"` + CheckForUpdates *bool `json:"checkForUpdates,omitempty"` } type RulesFile struct { @@ -212,6 +213,9 @@ func ToJSConfigScript(rf RulesFile, namespace string) (string, error) { if rf.Options.HideIcon != nil { opts["hideIcon"] = *rf.Options.HideIcon } + if rf.Options.HideWindowOnStart != nil { + opts["hideWindowOnStart"] = *rf.Options.HideWindowOnStart + } if rf.Options.LogRequests != nil { opts["logRequests"] = *rf.Options.LogRequests } diff --git a/apps/finicky/src/rules/rules_test.go b/apps/finicky/src/rules/rules_test.go index 0b81fc57..5070298f 100644 --- a/apps/finicky/src/rules/rules_test.go +++ b/apps/finicky/src/rules/rules_test.go @@ -124,6 +124,24 @@ func TestToJSConfigScript_NamespaceIsUsed(t *testing.T) { } } +// A rules file with only an options block (no defaultBrowser or rules) must +// still emit those options so flags like hideWindowOnStart take effect at +// startup, falling back to the default browser. +func TestToJSConfigScript_OptionsOnly(t *testing.T) { + hide := true + rf := RulesFile{Options: &Options{HideWindowOnStart: &hide}} + script, err := ToJSConfigScript(rf, "finickyConfig") + if err != nil { + t.Fatal(err) + } + if !contains(script, `"hideWindowOnStart":true`) { + t.Errorf("expected hideWindowOnStart option in script, got: %s", script) + } + if !contains(script, "com.apple.Safari") { + t.Errorf("expected fallback default browser in script, got: %s", script) + } +} + // ---- Load / Save round-trip ---- func TestLoadSave_RoundTrip(t *testing.T) { diff --git a/packages/config-api/src/configSchema.ts b/packages/config-api/src/configSchema.ts index cea27ae6..af7062ed 100644 --- a/packages/config-api/src/configSchema.ts +++ b/packages/config-api/src/configSchema.ts @@ -135,7 +135,11 @@ const ConfigOptionsSchema = z logRequests: z.boolean().optional().describe("Log to file on disk"), checkForUpdates: z.boolean().optional().describe("Check for updates"), keepRunning: z.boolean().optional().describe("Keep the app running"), - hideIcon: z.boolean().optional().describe("Hide the app icon") + hideIcon: z.boolean().optional().describe("Hide the app icon"), + hideWindowOnStart: z + .boolean() + .optional() + .describe("Don't open the window when Finicky starts") }) .identifier("ConfigOptions"); diff --git a/packages/finicky-ui/src/pages/StartPage.svelte b/packages/finicky-ui/src/pages/StartPage.svelte index 3c237c5b..92a56f38 100644 --- a/packages/finicky-ui/src/pages/StartPage.svelte +++ b/packages/finicky-ui/src/pages/StartPage.svelte @@ -30,6 +30,7 @@ // Local editable state, seeded from rules.json overrides then JS config then defaults let keepRunning = rulesFile.options?.keepRunning ?? config.options?.keepRunning ?? true; let hideIcon = rulesFile.options?.hideIcon ?? config.options?.hideIcon ?? false; + let hideWindowOnStart = rulesFile.options?.hideWindowOnStart ?? config.options?.hideWindowOnStart ?? false; let logRequests = rulesFile.options?.logRequests ?? config.options?.logRequests ?? false; let checkForUpdates = rulesFile.options?.checkForUpdates ?? config.options?.checkForUpdates ?? true; @@ -45,6 +46,7 @@ $: { keepRunning = rulesFile.options?.keepRunning ?? config.options?.keepRunning ?? true; hideIcon = rulesFile.options?.hideIcon ?? config.options?.hideIcon ?? false; + hideWindowOnStart = rulesFile.options?.hideWindowOnStart ?? config.options?.hideWindowOnStart ?? false; logRequests = rulesFile.options?.logRequests ?? config.options?.logRequests ?? false; checkForUpdates = rulesFile.options?.checkForUpdates ?? config.options?.checkForUpdates ?? true; defaultBrowser = isJSConfig ? (config.defaultBrowser ?? "") : (rulesFile.defaultBrowser || SAFARI); @@ -64,7 +66,7 @@ ...rulesFile, defaultBrowser, defaultProfile, - options: { keepRunning, hideIcon, logRequests, checkForUpdates }, + options: { keepRunning, hideIcon, hideWindowOnStart, logRequests, checkForUpdates }, }, }); } @@ -79,7 +81,7 @@ ...rulesFile, defaultBrowser, defaultProfile, - options: { keepRunning, hideIcon, logRequests, checkForUpdates }, + options: { keepRunning, hideIcon, hideWindowOnStart, logRequests, checkForUpdates }, }, }); }, SAVE_DEBOUNCE); @@ -164,6 +166,14 @@ onLockedClick={onLockedClick} onchange={scheduleSave} /> +