Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
4 changes: 2 additions & 2 deletions apps/finicky/assets/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>4.4.0-alpha</string>
<string>4.4.0-alpha+suppress-window</string>
<key>CFBundleVersion</key>
<string>4.4.0-alpha</string>
<string>4.4.0-alpha+suppress-window</string>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What are the allowed formats for CFBundleShortVersionString and CFBundleVersion in Apple's Info.plist? Is "4.4.0-alpha+suppress-window" valid for both keys?

💡 Result:

The string "4.4.0-alpha+suppress-window" is not a valid format for either CFBundleShortVersionString or CFBundleVersion [1][2]. According to Apple's current technical documentation, both keys must adhere to the following requirements: CFBundleShortVersionString This key represents the user-visible release version number of the bundle [1]. It must be a string consisting only of numeric characters (0-9) and periods, typically in a [Major].[Minor].[Patch] format (e.g., "10.14.1") [1]. CFBundleVersion This key represents the machine-readable build version number of the bundle [2]. It must be a string composed of one to three period-separated integers (e.g., "10.14.1") [2]. The string can only contain numeric characters (0-9) and periods [2]. While some older, legacy documentation mentioned support for specific alpha/beta suffixes, modern App Store submission requirements strictly enforce the numeric-only, period-separated integer format [2][3][4]. Using characters such as hyphens (-), plus signs (+), or alphabetic characters will cause validation errors during the submission process to App Store Connect [5][4]. To comply, you should use a purely numeric format for both keys, such as "4.4.0" [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Show the exact surrounding contents of the referenced plist
FILE="apps/finicky/assets/Info.plist"
nl -ba "$FILE" | sed -n '1,60p'

Repository: johnste/finicky

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/finicky/assets/Info.plist"

echo "=== lines 15-30 ==="
sed -n '15,30p' "$FILE"

echo "=== occurrences of 4.4.0-alpha+suppress-window ==="
rg -n "4\.4\.0-alpha\+suppress-window" "$FILE" || true

Repository: johnste/finicky

Length of output: 721


Fix CFBundleShortVersionString/CFBundleVersion to Apple-compliant numeric formats

apps/finicky/assets/Info.plist sets both keys to 4.4.0-alpha+suppress-window (lines 20-22). Apple requires:

  • CFBundleShortVersionString: digits and periods only (e.g., 4.4.0)
  • CFBundleVersion: 1–3 period-separated non-negative integers only

This value contains - and +, which will fail Info.plist/App Store validation. Use numeric-only values for these keys and move the alpha/branch metadata elsewhere.

<string>4.4.0-alpha+suppress-window</string>
<key>CFBundleVersion</key>
<string>4.4.0-alpha+suppress-window</string>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/finicky/assets/Info.plist` around lines 20 - 22, The Info.plist
currently sets CFBundleShortVersionString and CFBundleVersion to
"4.4.0-alpha+suppress-window", which is invalid for App Store validation; update
CFBundleShortVersionString to a digits-and-dots-only version like "4.4.0" and
set CFBundleVersion to a numeric build identifier with 1–3 dot-separated
non-negative integers (e.g., "4.4.0" or "440" depending on your build scheme)
and move the "alpha+suppress-window" pre-release/branch metadata out of these
keys (e.g., into a custom key or CI/build metadata) so
CFBundleShortVersionString and CFBundleVersion contain only numeric values.

<key>CFBundleIconFile</key>
<string>finicky.icns</string>
<key>LSUIElement</key>
Expand Down
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
16 changes: 10 additions & 6 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 @@ -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 */
23 changes: 18 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 @@ -233,6 +235,17 @@ - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
// If Finicky isn't frontmost, we take that to mean that the browser should, by default, be opened in the background
HandleURL((char*)url, (char*)name, (char*)bundleId, (char*)path, windowTitle, !finickyIsInFront);
free(windowTitle);

// When routing a URL while we weren't the active app, delivering the Apple
// Event can pull Finicky — and any open config window — to the foreground,
// briefly flashing it in front of the browser we're about to open. Order the
// window back and hand activation back so it stays out of the way. Deferred to
// the next runloop turn so it runs after any system-initiated activation.
if (!finickyIsInFront) {
dispatch_async(dispatch_get_main_queue(), ^{
[NSApp hide:nil];
});
}
}

- (bool)application:(NSApplication *)application willContinueUserActivityWithType:(NSString *)userActivityType {
Expand All @@ -259,12 +272,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
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