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}
/>
+