Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions apps/finicky/src/config/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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(),
}
}

Expand Down
18 changes: 11 additions & 7 deletions apps/finicky/src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

func handleRuntimeError(err error) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
},
})

Expand Down
5 changes: 3 additions & 2 deletions apps/finicky/src/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
12 changes: 7 additions & 5 deletions apps/finicky/src/main.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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];
Expand Down
12 changes: 8 additions & 4 deletions apps/finicky/src/rules/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
18 changes: 18 additions & 0 deletions apps/finicky/src/rules/rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 5 additions & 1 deletion packages/config-api/src/configSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
14 changes: 12 additions & 2 deletions packages/finicky-ui/src/pages/StartPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand All @@ -64,7 +66,7 @@
...rulesFile,
defaultBrowser,
defaultProfile,
options: { keepRunning, hideIcon, logRequests, checkForUpdates },
options: { keepRunning, hideIcon, hideWindowOnStart, logRequests, checkForUpdates },
},
});
}
Expand All @@ -79,7 +81,7 @@
...rulesFile,
defaultBrowser,
defaultProfile,
options: { keepRunning, hideIcon, logRequests, checkForUpdates },
options: { keepRunning, hideIcon, hideWindowOnStart, logRequests, checkForUpdates },
},
});
}, SAVE_DEBOUNCE);
Expand Down Expand Up @@ -164,6 +166,14 @@
onLockedClick={onLockedClick}
onchange={scheduleSave}
/>
<OptionRow
label="Hide window on start"
hint="Don't open the window when Finicky starts"
bind:checked={hideWindowOnStart}
locked={isJSConfig}
onLockedClick={onLockedClick}
onchange={scheduleSave}
/>
<OptionRow
label="Log requests"
hint="Log all URL handling to file"
Expand Down
1 change: 1 addition & 0 deletions packages/finicky-ui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface Rule {
export interface ConfigOptions {
keepRunning: boolean;
hideIcon: boolean;
hideWindowOnStart: boolean;
logRequests: boolean;
checkForUpdates: boolean;
}
Expand Down
7 changes: 7 additions & 0 deletions scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ build_arch() {
-o ../build/${APP_NAME}/Contents/MacOS/Finicky
}

# Remove stale .app bundles from previous builds. Without this, a second local

@fr33mang fr33mang Jun 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure that is needed that but locally I've stuck on rebuild because of some files left from previous build run

I can remove or create a separate PR for that code change

# build fails because `mv Finicky-arm64.app Finicky.app` nests the new bundle
# inside the existing Finicky.app directory instead of replacing it.
rm -rf apps/finicky/build/Finicky.app \
apps/finicky/build/Finicky-arm64.app \
apps/finicky/build/Finicky-amd64.app

if [ "${BUILD_UNIVERSAL:-0}" = "1" ]; then
build_arch arm64
build_arch amd64
Expand Down