From afe5443cb896edcefedbd1db38dffacbb55b672a Mon Sep 17 00:00:00 2001 From: John Sterling Date: Tue, 7 Oct 2025 20:31:08 +0200 Subject: [PATCH 1/6] feat: add 'edit config' menu option --- apps/finicky/src/main.go | 10 ++++++++++ apps/finicky/src/main.h | 1 + apps/finicky/src/main.m | 23 ++++++++++++++++++++++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/finicky/src/main.go b/apps/finicky/src/main.go index a578474..25889dc 100644 --- a/apps/finicky/src/main.go +++ b/apps/finicky/src/main.go @@ -371,6 +371,16 @@ func WindowDidClose() { windowClosed <- struct{}{} } +//export GetCurrentConfigPath +func GetCurrentConfigPath() *C.char { + if configInfo != nil && configInfo.ConfigPath != "" { + cPath := C.CString(configInfo.ConfigPath) + return cPath + } else { + return nil + } +} + func checkForUpdates() { var runtime *goja.Runtime if vm != nil { diff --git a/apps/finicky/src/main.h b/apps/finicky/src/main.h index 46e9490..fe1e983 100644 --- a/apps/finicky/src/main.h +++ b/apps/finicky/src/main.h @@ -12,6 +12,7 @@ extern void HandleURL(char *url, char *name, char *bundleId, char *path, bool openInBackground); extern void QueueWindowDisplay(int launchedByUser); extern void ShowConfigWindow(); +extern char* GetCurrentConfigPath(); #ifdef __OBJC__ @interface BrowseAppDelegate: NSObject diff --git a/apps/finicky/src/main.m b/apps/finicky/src/main.m index 9bfa0b8..af11695 100644 --- a/apps/finicky/src/main.m +++ b/apps/finicky/src/main.m @@ -67,7 +67,13 @@ - (void)createStatusItem { self.statusItem.button.toolTip = @"Finicky"; NSMenu *menu = [[NSMenu alloc] init]; [menu addItemWithTitle:@"Show Window" action:@selector(showWindowAction:) keyEquivalent:@""]; - [menu addItem:[NSMenuItem separatorItem]]; + + char* configPath = GetCurrentConfigPath(); + if (configPath) { + [menu addItemWithTitle:@"Edit config" action:@selector(editConfigAction:) keyEquivalent:@""]; + [menu addItem:[NSMenuItem separatorItem]]; + } + [menu addItemWithTitle:@"Quit" action:@selector(terminate:) keyEquivalent:@"q"]; self.statusItem.menu = menu; } @@ -77,6 +83,21 @@ - (void)showWindowAction:(id)sender { ShowConfigWindow(); } + +-(void)editConfigAction:(id)sender { + char* configPath = GetCurrentConfigPath(); + + if (configPath) { + NSString *path = [NSString stringWithUTF8String:configPath]; + free(configPath); // Free the C string after converting to NSString + + if (path && [path length] > 0) { + NSURL *fileURL = [NSURL fileURLWithPath:path]; + [[NSWorkspace sharedWorkspace] openURL:fileURL]; + } + } +} + - (void)applicationWillFinishLaunching:(NSNotification *)aNotification { NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; From 3a5e1e13ec70011778454d8b97e41468aab8feca Mon Sep 17 00:00:00 2001 From: John Sterling Date: Tue, 7 Oct 2025 21:30:41 +0200 Subject: [PATCH 2/6] feat: update ui --- apps/finicky/src/main.go | 42 +++- apps/finicky/src/window/window.m | 21 ++ domains-and-urls.txt | 19 ++ packages/finicky-ui/src/App.svelte | 63 ++++- .../src/components/StartPage.svelte | 186 +++++++++++++-- .../finicky-ui/src/components/TabBar.svelte | 6 + .../finicky-ui/src/components/TestUrl.svelte | 220 ++++++++++++++++++ .../src/components/icons/Test.svelte | 5 + packages/finicky-ui/src/types.ts | 18 ++ 9 files changed, 554 insertions(+), 26 deletions(-) create mode 100644 domains-and-urls.txt create mode 100644 packages/finicky-ui/src/components/TestUrl.svelte create mode 100644 packages/finicky-ui/src/components/icons/Test.svelte diff --git a/apps/finicky/src/main.go b/apps/finicky/src/main.go index 25889dc..9dcdd1b 100644 --- a/apps/finicky/src/main.go +++ b/apps/finicky/src/main.go @@ -462,18 +462,44 @@ func setupVM(cfw *config.ConfigFileWatcher, embeddedFS embed.FS, namespace strin ConfigPath: configPath, } + keepRunning := getKeepRunning() + hideIcon := getHideIcon() + + // Get logRequests option + logRequests := false + if logRequestsVal, err := vm.Runtime().RunString("finickyConfigAPI.getOption('logRequests', finalConfig, false)"); err == nil { + logRequests = logRequestsVal.ToBoolean() + } + + // Get checkForUpdate option + checkForUpdate := true + if checkForUpdateVal, err := vm.Runtime().RunString("finickyConfigAPI.getOption('checkForUpdate', finalConfig, true)"); err == nil { + checkForUpdate = checkForUpdateVal.ToBoolean() + } + window.SendMessageToWebView("config", map[string]interface{}{ - "handlers": configInfo.Handlers, - "rewrites": configInfo.Rewrites, - "defaultBrowser": configInfo.DefaultBrowser, - "configPath": configInfo.ConfigPath, + "handlers": configInfo.Handlers, + "rewrites": configInfo.Rewrites, + "defaultBrowser": configInfo.DefaultBrowser, + "configPath": configInfo.ConfigPath, + "keepRunning": keepRunning, + "hideIcon": hideIcon, + "logRequests": logRequests, + "checkForUpdate": checkForUpdate, }) } else if configInfo != nil { + keepRunning := getKeepRunning() + hideIcon := getHideIcon() + window.SendMessageToWebView("config", map[string]interface{}{ - "handlers": 0, - "rewrites": 0, - "defaultBrowser": "", - "configPath": configInfo.ConfigPath, + "handlers": 0, + "rewrites": 0, + "defaultBrowser": "", + "configPath": configInfo.ConfigPath, + "keepRunning": keepRunning, + "hideIcon": hideIcon, + "logRequests": false, + "checkForUpdate": true, }) } diff --git a/apps/finicky/src/window/window.m b/apps/finicky/src/window/window.m index 1350766..bb2d5e8 100644 --- a/apps/finicky/src/window/window.m +++ b/apps/finicky/src/window/window.m @@ -277,11 +277,32 @@ - (void)setupMenu { NSMenu *editMenu = [[NSMenu alloc] initWithTitle:@"Edit"]; [editMenuItem setSubmenu:editMenu]; + // Add Cut menu item (⌘X) + NSMenuItem *cutMenuItem = [[NSMenuItem alloc] initWithTitle:@"Cut" + action:@selector(cut:) + keyEquivalent:@"x"]; + [editMenu addItem:cutMenuItem]; + // Add Copy menu item (⌘C) NSMenuItem *copyMenuItem = [[NSMenuItem alloc] initWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"]; [editMenu addItem:copyMenuItem]; + + // Add Paste menu item (⌘V) + NSMenuItem *pasteMenuItem = [[NSMenuItem alloc] initWithTitle:@"Paste" + action:@selector(paste:) + keyEquivalent:@"v"]; + [editMenu addItem:pasteMenuItem]; + + // Add separator + [editMenu addItem:[NSMenuItem separatorItem]]; + + // Add Select All menu item (⌘A) + NSMenuItem *selectAllMenuItem = [[NSMenuItem alloc] initWithTitle:@"Select All" + action:@selector(selectAll:) + keyEquivalent:@"a"]; + [editMenu addItem:selectAllMenuItem]; } @end diff --git a/domains-and-urls.txt b/domains-and-urls.txt new file mode 100644 index 0000000..bf84811 --- /dev/null +++ b/domains-and-urls.txt @@ -0,0 +1,19 @@ +https://example.com +http://example.com +https://example.com:8080 +http://example.com:3000/path +https://www.example.com/path/to/resource +http://subdomain.example.com +https://subdomain.example.com:8443/another/path?query=1 +ftp://example.org/resource.txt +http://test.example.net:1234 +https://example.com/path/to/file.html +http://example.com?search=test +https://example.com:443/ +http://localhost:8000/ +https://127.0.0.1:5000/api +http://example.edu/resource#section +https://another-example.com:9090/path?foo=bar&baz=qux +file:///Users/john/Documents/example.txt +https://fully.qualified.domain.name/ +https://example.com./ \ No newline at end of file diff --git a/packages/finicky-ui/src/App.svelte b/packages/finicky-ui/src/App.svelte index 29b5335..c1da936 100644 --- a/packages/finicky-ui/src/App.svelte +++ b/packages/finicky-ui/src/App.svelte @@ -5,7 +5,8 @@ import StartPage from "./components/StartPage.svelte"; import TabBar from "./components/TabBar.svelte"; import About from "./components/About.svelte"; - import type { LogEntry, UpdateInfo } from "./types"; + import TestUrl from "./components/TestUrl.svelte"; + import type { LogEntry, UpdateInfo, ConfigInfo } from "./types"; let version = "v0.0.0"; let buildInfo = "dev"; @@ -19,7 +20,7 @@ // Configuration state let hasConfig = false; - let config: { configPath: string } = { configPath: "" }; + let config: ConfigInfo = { configPath: "" }; // Initialize message buffer let messageBuffer: LogEntry[] = []; let updateInfo: UpdateInfo | null = null; @@ -99,7 +100,7 @@ @@ -108,6 +109,10 @@ + + + + @@ -193,13 +169,6 @@ flex: 1; } - .dev-mode { - color: #b654ff; - font-weight: bold; - font-size: 0.8em; - opacity: 0.8; - } - .spacer { flex: 1; } diff --git a/packages/finicky-ui/src/components/About.svelte b/packages/finicky-ui/src/components/About.svelte index eae8449..cc9cabd 100644 --- a/packages/finicky-ui/src/components/About.svelte +++ b/packages/finicky-ui/src/components/About.svelte @@ -1,141 +1,111 @@ -
-
-
- -
+
+
+ + + Finicky icon + +

{version}

-
- Developer mode + -

{version}

-
+ -

- Created by John Sterling -
- Icon designed by - @uetchy + Icon designed by @uetchy

- {#if isDevMode} - - {/if}
From 2a9568532dce9a5394be670c99a87de4db56a2e3 Mon Sep 17 00:00:00 2001 From: John Sterling Date: Fri, 10 Oct 2025 21:51:43 +0200 Subject: [PATCH 4/6] feat: constrain window size --- apps/finicky/src/window/window.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/finicky/src/window/window.m b/apps/finicky/src/window/window.m index bb2d5e8..02335fb 100644 --- a/apps/finicky/src/window/window.m +++ b/apps/finicky/src/window/window.m @@ -75,6 +75,10 @@ - (void)setupWindow { [window setReleasedWhenClosed:NO]; [window setBackgroundColor:[NSColor colorWithCalibratedWhite:0.1 alpha:1.0]]; + // Set minimum window size + [window setMinSize:NSMakeSize(800, 500)]; + [window setMaxSize:NSMakeSize(1200, 900)]; + // Configure WKWebView WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; [config.userContentController addScriptMessageHandler:self name:@"finicky"]; From 11a2f41f625ed915a187533d597b712144219223 Mon Sep 17 00:00:00 2001 From: John Sterling Date: Fri, 10 Oct 2025 21:55:50 +0200 Subject: [PATCH 5/6] feat: rename troubleshoot to logs --- .../finicky-ui/src/components/TabBar.svelte | 7 +++---- .../src/components/icons/Logs.svelte | 6 ++++++ .../src/components/icons/Troubleshoot.svelte | 21 ------------------- 3 files changed, 9 insertions(+), 25 deletions(-) create mode 100644 packages/finicky-ui/src/components/icons/Logs.svelte delete mode 100644 packages/finicky-ui/src/components/icons/Troubleshoot.svelte diff --git a/packages/finicky-ui/src/components/TabBar.svelte b/packages/finicky-ui/src/components/TabBar.svelte index c669615..20dcaa9 100644 --- a/packages/finicky-ui/src/components/TabBar.svelte +++ b/packages/finicky-ui/src/components/TabBar.svelte @@ -2,7 +2,7 @@ import { Link } from "svelte-routing"; import General from "./icons/General.svelte"; import Test from "./icons/Test.svelte"; - import Troubleshoot from "./icons/Troubleshoot.svelte"; + import Logs from "./icons/Logs.svelte"; import About from "./icons/About.svelte"; export let numErrors: number = 0; @@ -20,9 +20,9 @@ }, { path: "/troubleshoot", - label: "Troubleshoot", + label: "Logs", showErrors: true, - component: Troubleshoot, + component: Logs, }, { path: "/about", @@ -66,7 +66,6 @@ } .tab-container { - width: 190px; background: var(--bg-primary); position: sticky; diff --git a/packages/finicky-ui/src/components/icons/Logs.svelte b/packages/finicky-ui/src/components/icons/Logs.svelte new file mode 100644 index 0000000..edfbec7 --- /dev/null +++ b/packages/finicky-ui/src/components/icons/Logs.svelte @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/finicky-ui/src/components/icons/Troubleshoot.svelte b/packages/finicky-ui/src/components/icons/Troubleshoot.svelte deleted file mode 100644 index 9362df8..0000000 --- a/packages/finicky-ui/src/components/icons/Troubleshoot.svelte +++ /dev/null @@ -1,21 +0,0 @@ - From e08e8e62c08d121a36d598a66f31a755387dcd89 Mon Sep 17 00:00:00 2001 From: John Sterling Date: Fri, 10 Oct 2025 22:51:25 +0200 Subject: [PATCH 6/6] feat: clean up app start page and css --- apps/finicky/src/config/configfiles.go | 6 + apps/finicky/src/config/vm.go | 16 - apps/finicky/src/go.mod | 10 +- apps/finicky/src/go.sum | 8 + apps/finicky/src/main.go | 165 +++++----- apps/finicky/src/shorturl/resolver.go | 24 +- apps/finicky/src/window/window.go | 46 +++ apps/finicky/src/window/window.m | 30 ++ packages/config-api/src/config.test.ts | 12 +- packages/config-api/src/configSchema.ts | 1 + packages/config-api/src/index.ts | 19 ++ packages/finicky-ui/src/App.svelte | 47 ++- packages/finicky-ui/src/app.css | 4 +- .../finicky-ui/src/components/About.svelte | 116 ------- .../src/components/LogContent.svelte | 15 +- .../src/components/LogViewer.svelte | 141 --------- .../src/components/PageContainer.svelte | 62 ++++ .../finicky-ui/src/components/TabBar.svelte | 18 +- .../finicky-ui/src/components/TestUrl.svelte | 220 ------------- .../src/components/ToastContainer.svelte | 170 ++++++++++ .../finicky-ui/src/components/Toggle.svelte | 0 .../src/components/icons/External.svelte | 15 + .../src/components/icons/Info.svelte | 15 + .../src/components/icons/Link.svelte | 14 + .../{General.svelte => Preferences.svelte} | 0 .../src/components/icons/Spinner.svelte | 12 + packages/finicky-ui/src/lib/testUrlStore.ts | 10 + packages/finicky-ui/src/lib/toast.ts | 105 ++++++ packages/finicky-ui/src/pages/About.svelte | 168 ++++++++++ .../finicky-ui/src/pages/LogViewer.svelte | 160 ++++++++++ .../{components => pages}/StartPage.svelte | 186 ++++++----- packages/finicky-ui/src/pages/TestUrl.svelte | 299 ++++++++++++++++++ packages/finicky-ui/src/types.ts | 12 +- 33 files changed, 1420 insertions(+), 706 deletions(-) delete mode 100644 packages/finicky-ui/src/components/About.svelte delete mode 100644 packages/finicky-ui/src/components/LogViewer.svelte create mode 100644 packages/finicky-ui/src/components/PageContainer.svelte delete mode 100644 packages/finicky-ui/src/components/TestUrl.svelte create mode 100644 packages/finicky-ui/src/components/ToastContainer.svelte create mode 100644 packages/finicky-ui/src/components/Toggle.svelte create mode 100644 packages/finicky-ui/src/components/icons/External.svelte create mode 100644 packages/finicky-ui/src/components/icons/Info.svelte create mode 100644 packages/finicky-ui/src/components/icons/Link.svelte rename packages/finicky-ui/src/components/icons/{General.svelte => Preferences.svelte} (100%) create mode 100644 packages/finicky-ui/src/components/icons/Spinner.svelte create mode 100644 packages/finicky-ui/src/lib/testUrlStore.ts create mode 100644 packages/finicky-ui/src/lib/toast.ts create mode 100644 packages/finicky-ui/src/pages/About.svelte create mode 100644 packages/finicky-ui/src/pages/LogViewer.svelte rename packages/finicky-ui/src/{components => pages}/StartPage.svelte (57%) create mode 100644 packages/finicky-ui/src/pages/TestUrl.svelte diff --git a/apps/finicky/src/config/configfiles.go b/apps/finicky/src/config/configfiles.go index 0df7c27..0a67ce3 100644 --- a/apps/finicky/src/config/configfiles.go +++ b/apps/finicky/src/config/configfiles.go @@ -333,6 +333,12 @@ func (cfw *ConfigFileWatcher) StartWatching() error { // handleConfigFileEvent processes configuration file events and takes appropriate actions // Returns an error if the configuration file was removed func (cfw *ConfigFileWatcher) handleConfigFileEvent(event fsnotify.Event) error { + // Ignore CHMOD-only events (permission changes) as they don't affect config content + // Note: Some editors may send CHMOD along with WRITE, so we only ignore pure CHMOD + if event.Op == fsnotify.Chmod { + return nil + } + if event.Has(fsnotify.Create) { slog.Debug("Configuration file created", "path", event.Name) } diff --git a/apps/finicky/src/config/vm.go b/apps/finicky/src/config/vm.go index 01e4639..a8085e0 100644 --- a/apps/finicky/src/config/vm.go +++ b/apps/finicky/src/config/vm.go @@ -100,22 +100,6 @@ func (vm *VM) setup(embeddedFiles embed.FS, bundlePath string) error { return nil } -func (vm *VM) ShouldLogToFile(hasError bool) bool { - - logRequests := vm.runtime.ToValue(hasError) - - if !hasError { - var err error - logRequests, err = vm.runtime.RunString("finickyConfigAPI.getOption('logRequests', finalConfig)") - if err != nil { - slog.Warn("Failed to get logRequests option", "error", err) - logRequests = vm.runtime.ToValue(true) - } - } - - return logRequests.ToBoolean() -} - func (vm *VM) GetConfigState() *ConfigState { state, err := vm.runtime.RunString("finickyConfigAPI.getConfigState(finalConfig)") if err != nil { diff --git a/apps/finicky/src/go.mod b/apps/finicky/src/go.mod index 20cb0ff..26045d8 100644 --- a/apps/finicky/src/go.mod +++ b/apps/finicky/src/go.mod @@ -1,11 +1,11 @@ module finicky -go 1.23.4 +go 1.24.0 require ( al.essio.dev/pkg/shellescape v1.6.0 github.com/Masterminds/semver v1.5.0 - github.com/dop251/goja v0.0.0-20250307175808-203961f822d6 + github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6 github.com/evanw/esbuild v0.24.2 github.com/fsnotify/fsnotify v1.8.0 github.com/jvatic/goja-babel v0.0.0-20250308121736-c08d87dbdc10 @@ -14,7 +14,7 @@ require ( require ( github.com/dlclark/regexp2 v1.11.5 // indirect github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect - github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.23.0 // indirect + github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.30.0 // indirect ) diff --git a/apps/finicky/src/go.sum b/apps/finicky/src/go.sum index c518c1c..78e0c10 100644 --- a/apps/finicky/src/go.sum +++ b/apps/finicky/src/go.sum @@ -8,6 +8,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dop251/goja v0.0.0-20250307175808-203961f822d6 h1:G73yPVwEaihFs6WYKFFfSstwNY2vENyECvRnR0tye0g= github.com/dop251/goja v0.0.0-20250307175808-203961f822d6/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= +github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6 h1:6dE1TmjqkY6tehR4A67gDNhvDtuZ54ocu7ab4K9o540= +github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= github.com/evanw/esbuild v0.24.2 h1:PQExybVBrjHjN6/JJiShRGIXh1hWVm6NepVnhZhrt0A= github.com/evanw/esbuild v0.24.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= @@ -16,6 +18,8 @@ github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TC github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro= github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0= +github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/jvatic/goja-babel v0.0.0-20250308121736-c08d87dbdc10 h1:vWIOMaPN3MJ9W2U+0sKPoouDoSdRP6TNEfmfmT8AmfA= @@ -31,7 +35,11 @@ github.com/stvp/assert v0.0.0-20170616060220-4bc16443988b/go.mod h1:CC7OXV9IjEZR golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/apps/finicky/src/main.go b/apps/finicky/src/main.go index 9dcdd1b..459af0d 100644 --- a/apps/finicky/src/main.go +++ b/apps/finicky/src/main.go @@ -126,13 +126,18 @@ func main() { go checkForUpdates() + // Set up test URL handler + window.TestUrlHandler = func(url string) { + go TestURLInternal(url) + } + const oneDay = 24 * time.Hour var showingWindow bool = false timeoutChan := time.After(1 * time.Second) updateChan := time.After(oneDay) - shouldKeepRunning = getKeepRunning() + shouldKeepRunning = getConfigOption("keepRunning", true) if shouldKeepRunning { timeoutChan = nil } @@ -186,12 +191,13 @@ func main() { case <-configChange: startTime := time.Now() var setupErr error + slog.Debug("Config has changed") vm, setupErr = setupVM(cfw, embeddedFiles, namespace) if setupErr != nil { handleRuntimeError(setupErr) } slog.Debug("VM refresh complete", "duration", fmt.Sprintf("%.2fms", float64(time.Since(startTime).Microseconds())/1000)) - shouldKeepRunning = getKeepRunning() + shouldKeepRunning = getConfigOption("keepRunning", true) case shouldShowWindow := <-queueWindowOpen: if !showingWindow && shouldShowWindow { @@ -219,8 +225,9 @@ func main() { } }() - showIcon := !getHideIcon() - C.RunApp(C.bool(forceWindowOpen), C.bool(showIcon), C.bool(shouldKeepRunning)) + hideIcon := getConfigOption("hideIcon", false) + + C.RunApp(C.bool(forceWindowOpen), C.bool(!hideIcon), C.bool(shouldKeepRunning)) } func handleRuntimeError(err error) { @@ -229,32 +236,21 @@ func handleRuntimeError(err error) { go QueueWindowDisplay(1) } -func getKeepRunning() bool { - if vm == nil { - return false - } - - keepRunning, err := vm.Runtime().RunString("finickyConfigAPI.getOption('keepRunning', finalConfig, true)") - if err != nil { - return false +func getConfigOption(optionName string, defaultValue bool) bool { + if vm == nil || vm.Runtime() == nil { + slog.Debug("VM not initialized, returning default for config option", "option", optionName, "default", defaultValue) + return defaultValue } - result := keepRunning.ToBoolean() - return result -} - -func getHideIcon() bool { - if vm == nil { - return false - } + script := fmt.Sprintf("finickyConfigAPI.getOption('%s', finalConfig, %t)", optionName, defaultValue) + optionVal, err := vm.Runtime().RunString(script) - hideIcon, err := vm.Runtime().RunString("finickyConfigAPI.getOption('hideIcon', finalConfig, false)") if err != nil { - return false + slog.Error("Failed to get config option", "option", optionName, "error", err) + return defaultValue } - result := hideIcon.ToBoolean() - return result + return optionVal.ToBoolean() } //export HandleURL @@ -289,16 +285,59 @@ func HandleURL(url *C.char, name *C.char, bundleId *C.char, path *C.char, openIn } } +//export TestURL +func TestURL(url *C.char) { + urlString := C.GoString(url) + TestURLInternal(urlString) +} + +func TestURLInternal(urlString string) { + slog.Debug("Testing URL", "url", urlString) + + if vm == nil { + slog.Error("VM not initialized") + window.SendMessageToWebView("testUrlResult", map[string]interface{}{ + "error": "Configuration not loaded", + }) + return + } + + browserConfig, err := evaluateURL(vm.Runtime(), urlString, nil) + if err != nil { + slog.Error("Failed to evaluate URL", "error", err) + window.SendMessageToWebView("testUrlResult", map[string]interface{}{ + "error": err.Error(), + }) + return + } + + if browserConfig == nil { + window.SendMessageToWebView("testUrlResult", map[string]interface{}{ + "error": "No browser config returned", + }) + return + } + + window.SendMessageToWebView("testUrlResult", map[string]interface{}{ + "url": browserConfig.URL, + "browser": browserConfig.Name, + "openInBackground": browserConfig.OpenInBackground, + "profile": browserConfig.Profile, + "args": browserConfig.Args, + }) +} + func evaluateURL(vm *goja.Runtime, url string, opener *ProcessInfo) (*browser.BrowserConfig, error) { resolvedURL, err := shorturl.ResolveURL(url) + vm.Set("originalUrl", url) + if err != nil { // Continue with original URL if resolution fails - slog.Info("Failed to resolve short URL", "error", err) - - } else { - url = resolvedURL + slog.Info("Failed to resolve short URL", "error", err, "url", url, "using", resolvedURL) } + url = resolvedURL + vm.Set("url", resolvedURL) if opener != nil { @@ -313,7 +352,7 @@ func evaluateURL(vm *goja.Runtime, url string, opener *ProcessInfo) (*browser.Br slog.Debug("No opener detected") } - openResult, err := vm.RunString("finickyConfigAPI.openUrl(url, opener, finalConfig)") + openResult, err := vm.RunString("finickyConfigAPI.openUrl(url, opener, originalUrl, finalConfig)") if err != nil { return nil, fmt.Errorf("failed to evaluate URL in config: %v", err) } @@ -427,11 +466,11 @@ func tearDown() { } func setupVM(cfw *config.ConfigFileWatcher, embeddedFS embed.FS, namespace string) (*config.VM, error) { - shouldLogToFile := true + logRequests := true var err error defer func() { - err = logger.SetupFile(shouldLogToFile) + err = logger.SetupFile(logRequests) if err != nil { slog.Warn("Failed to setup file logging", "error", err) } @@ -444,14 +483,12 @@ func setupVM(cfw *config.ConfigFileWatcher, embeddedFS embed.FS, namespace strin } if currentBundlePath != "" { - vm, err := config.New(embeddedFS, namespace, currentBundlePath) + vm, err = config.New(embeddedFS, namespace, currentBundlePath) + if err != nil { return nil, fmt.Errorf("failed to setup VM: %v", err) } - // Update logging preference based on VM if available - shouldLogToFile = vm.ShouldLogToFile(false) - currentConfigState = vm.GetConfigState() if currentConfigState != nil { @@ -461,48 +498,26 @@ func setupVM(cfw *config.ConfigFileWatcher, embeddedFS embed.FS, namespace strin DefaultBrowser: currentConfigState.DefaultBrowser, ConfigPath: configPath, } - - keepRunning := getKeepRunning() - hideIcon := getHideIcon() - - // Get logRequests option - logRequests := false - if logRequestsVal, err := vm.Runtime().RunString("finickyConfigAPI.getOption('logRequests', finalConfig, false)"); err == nil { - logRequests = logRequestsVal.ToBoolean() - } - - // Get checkForUpdate option - checkForUpdate := true - if checkForUpdateVal, err := vm.Runtime().RunString("finickyConfigAPI.getOption('checkForUpdate', finalConfig, true)"); err == nil { - checkForUpdate = checkForUpdateVal.ToBoolean() - } - - window.SendMessageToWebView("config", map[string]interface{}{ - "handlers": configInfo.Handlers, - "rewrites": configInfo.Rewrites, - "defaultBrowser": configInfo.DefaultBrowser, - "configPath": configInfo.ConfigPath, - "keepRunning": keepRunning, - "hideIcon": hideIcon, - "logRequests": logRequests, - "checkForUpdate": checkForUpdate, - }) - } else if configInfo != nil { - keepRunning := getKeepRunning() - hideIcon := getHideIcon() - - window.SendMessageToWebView("config", map[string]interface{}{ - "handlers": 0, - "rewrites": 0, - "defaultBrowser": "", - "configPath": configInfo.ConfigPath, - "keepRunning": keepRunning, - "hideIcon": hideIcon, - "logRequests": false, - "checkForUpdate": true, - }) } + keepRunning := getConfigOption("keepRunning", true) + hideIcon := getConfigOption("hideIcon", false) + logRequests = getConfigOption("logRequests", false) + checkForUpdates := getConfigOption("checkForUpdates", true) + + window.SendMessageToWebView("config", map[string]interface{}{ + "handlers": configInfo.Handlers, + "rewrites": configInfo.Rewrites, + "defaultBrowser": configInfo.DefaultBrowser, + "configPath": configInfo.ConfigPath, + "options": map[string]interface{}{ + "keepRunning": keepRunning, + "hideIcon": hideIcon, + "logRequests": logRequests, + "checkForUpdates": checkForUpdates, + }, + }) + return vm, nil } diff --git a/apps/finicky/src/shorturl/resolver.go b/apps/finicky/src/shorturl/resolver.go index 4773916..e61de51 100644 --- a/apps/finicky/src/shorturl/resolver.go +++ b/apps/finicky/src/shorturl/resolver.go @@ -57,11 +57,23 @@ func ResolveURL(originalURL string) (string, error) { slog.Debug("URL host looks like a short URL", "host", parsedURL.Host) + var lastUrl string + + // Helper to get the best available URL + getReturnUrl := func() string { + if lastUrl != "" && lastUrl != originalURL { + slog.Debug("Falling back to last known URL from redirects", "url", lastUrl) + return lastUrl + } + return originalURL + } + // Create a client with a timeout client := &http.Client{ - Timeout: 500 * time.Millisecond, + Timeout: 750 * time.Millisecond, CheckRedirect: func(req *http.Request, via []*http.Request) error { - slog.Debug("Redirected to", "url", req.URL.String()) + lastUrl = req.URL.String() + slog.Debug("Redirected to", "url", lastUrl) // Allow up to 3 redirects if len(via) >= 3 { return fmt.Errorf("stopped after 3 redirects") @@ -78,6 +90,7 @@ func ResolveURL(originalURL string) (string, error) { req.Header.Set("User-Agent", "Finicky/4.0") resp, err := client.Do(req) + if err != nil { slog.Debug("Failed to make HEAD request", "url", originalURL, "error", err) } @@ -97,13 +110,14 @@ func ResolveURL(originalURL string) (string, error) { // If HEAD request failed, try GET as fallback req, err = http.NewRequest("GET", originalURL, nil) if err != nil { - return originalURL, fmt.Errorf("failed to create GET request: %v", err) + return getReturnUrl(), fmt.Errorf("failed to create GET request: %v", err) } req.Header.Set("User-Agent", "Finicky/4.0") resp, err = client.Do(req) + if err != nil { - return originalURL, fmt.Errorf("failed to make GET request: %v", err) + return getReturnUrl(), fmt.Errorf("failed to make GET request: %v", err) } if resp != nil { @@ -118,6 +132,6 @@ func ResolveURL(originalURL string) (string, error) { } // If both HEAD and GET failed, return original URL - return originalURL, fmt.Errorf("failed to resolve URL: no response received") + return getReturnUrl(), fmt.Errorf("failed to resolve URL: no response received") } diff --git a/apps/finicky/src/window/window.go b/apps/finicky/src/window/window.go index 8b796a6..c1ba015 100644 --- a/apps/finicky/src/window/window.go +++ b/apps/finicky/src/window/window.go @@ -25,6 +25,7 @@ var ( messageQueue []string queueMutex sync.Mutex windowReady bool + TestUrlHandler func(string) ) //export WindowIsReady @@ -136,3 +137,48 @@ func SendBuildInfo() { buildInfo := fmt.Sprintf("(%s, built %s)", commitHash, buildDate) SendMessageToWebView("buildInfo", buildInfo) } + +//export HandleWebViewMessage +func HandleWebViewMessage(messagePtr *C.char) { + messageStr := C.GoString(messagePtr) + + var msg map[string]interface{} + if err := json.Unmarshal([]byte(messageStr), &msg); err != nil { + slog.Error("Failed to parse webview message", "error", err) + return + } + + messageType, ok := msg["type"].(string) + if !ok { + slog.Error("Message missing type field") + return + } + + slog.Debug("Received message from webview", "type", messageType) + + switch messageType { + case "testUrl": + handleTestUrl(msg) + default: + slog.Debug("Unknown message type", "type", messageType) + } +} + +func handleTestUrl(msg map[string]interface{}) { + url, ok := msg["url"].(string) + if !ok { + slog.Error("testUrl message missing url field") + return + } + + slog.Debug("Forwarding test URL request", "url", url) + + if TestUrlHandler != nil { + TestUrlHandler(url) + } else { + slog.Error("TestUrlHandler not set") + SendMessageToWebView("testUrlResult", map[string]interface{}{ + "error": "Test handler not initialized", + }) + } +} diff --git a/apps/finicky/src/window/window.m b/apps/finicky/src/window/window.m index 02335fb..a3a941e 100644 --- a/apps/finicky/src/window/window.m +++ b/apps/finicky/src/window/window.m @@ -156,6 +156,21 @@ - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { // Handle messages from JavaScript here NSLog(@"Received message from WebView: %@", message.body); + + // Convert the message body to JSON string and forward to Go + if ([message.body isKindOfClass:[NSString class]]) { + extern void HandleWebViewMessage(const char* message); + NSString *messageString = (NSString *)message.body; + HandleWebViewMessage([messageString UTF8String]); + } else if ([message.body isKindOfClass:[NSDictionary class]]) { + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:message.body options:0 error:&error]; + if (jsonData && !error) { + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + extern void HandleWebViewMessage(const char* message); + HandleWebViewMessage([jsonString UTF8String]); + } + } } #pragma mark - WKURLSchemeHandler @@ -302,6 +317,21 @@ - (void)setupMenu { // Add separator [editMenu addItem:[NSMenuItem separatorItem]]; + // Add Undo menu item (⌘Z) + NSMenuItem *undoMenuItem = [[NSMenuItem alloc] initWithTitle:@"Undo" + action:@selector(undo:) + keyEquivalent:@"z"]; + [editMenu addItem:undoMenuItem]; + + // Add Redo menu item (⌘⇧Z) + NSMenuItem *redoMenuItem = [[NSMenuItem alloc] initWithTitle:@"Redo" + action:@selector(redo:) + keyEquivalent:@"Z"]; + [editMenu addItem:redoMenuItem]; + + // Add separator + [editMenu addItem:[NSMenuItem separatorItem]]; + // Add Select All menu item (⌘A) NSMenuItem *selectAllMenuItem = [[NSMenuItem alloc] initWithTitle:@"Select All" action:@selector(selectAll:) diff --git a/packages/config-api/src/config.test.ts b/packages/config-api/src/config.test.ts index b935cc9..57b03be 100644 --- a/packages/config-api/src/config.test.ts +++ b/packages/config-api/src/config.test.ts @@ -11,7 +11,7 @@ describe("openUrl", () => { it("validation", () => { expect(() => - openUrl("https://example.com", mockProcessInfo, { + openUrl("https://example.com", mockProcessInfo, null, { defaultBrowser: 123, }) ).toThrow("Invalid config"); @@ -52,7 +52,7 @@ describe("openUrl", () => { cases.forEach(({ url, expected }) => { it(`handles ${url}`, () => { - const result = openUrl(url, mockProcessInfo, handlerConfig); + const result = openUrl(url, mockProcessInfo, null, handlerConfig); expect(result.browser).toMatchObject( typeof expected === "string" ? { name: expected } : expected ); @@ -60,7 +60,7 @@ describe("openUrl", () => { }); it("works with null opener", () => { - const result = openUrl("https://example.com", null, handlerConfig); + const result = openUrl("https://example.com", null, null, handlerConfig); expect(result.browser).toMatchObject({ name: "Firefox" }); }); }); @@ -109,7 +109,7 @@ describe("openUrl", () => { cases.forEach(({ input, expectedUrl }) => { it(`rewrites ${input}`, () => { - const result = openUrl(input, mockProcessInfo, rewriteConfig); + const result = openUrl(input, mockProcessInfo, null, rewriteConfig); expect(result.browser).toMatchObject({ name: "Safari", url: expectedUrl, @@ -149,7 +149,7 @@ describe("openUrl", () => { cases.forEach(({ urls, expected }) => { urls.forEach((url) => { it(`${expected} handles ${url}`, () => { - const result = openUrl(url, mockProcessInfo, wildcardConfig); + const result = openUrl(url, mockProcessInfo, null, wildcardConfig); expect(result.browser).toMatchObject({ name: expected }); }); }); @@ -174,7 +174,7 @@ describe("openUrl", () => { cases.forEach(({ url, expected }) => { it(`${expected} handles ${url}`, () => { - const result = openUrl(url, mockProcessInfo, edgeCaseConfig); + const result = openUrl(url, mockProcessInfo, null, edgeCaseConfig); expect(result.browser).toMatchObject({ name: expected }); }); }); diff --git a/packages/config-api/src/configSchema.ts b/packages/config-api/src/configSchema.ts index 40f8072..b117067 100644 --- a/packages/config-api/src/configSchema.ts +++ b/packages/config-api/src/configSchema.ts @@ -39,6 +39,7 @@ export type ProcessInfo = z.infer; const OpenUrlOptionsSchema = z .object({ opener: ProcessInfoSchema.nullable(), + originalUrl: NativeUrlSchema.optional(), }) .identifier("OpenUrlOptions"); diff --git a/packages/config-api/src/index.ts b/packages/config-api/src/index.ts index aa7fc99..4723550 100644 --- a/packages/config-api/src/index.ts +++ b/packages/config-api/src/index.ts @@ -82,8 +82,10 @@ export function getConfigState(config: Config) { export function openUrl( urlString: string, opener: ProcessInfo | null, + originalUrlString: string | null, config: object ) { + try { if (!validateConfig(config)) { throw new Error("Invalid config"); } @@ -94,6 +96,10 @@ export function openUrl( opener: opener, }; + if (originalUrlString) { + options.originalUrl = new FinickyURL(originalUrlString, opener); + } + let error: string | undefined; try { @@ -124,6 +130,19 @@ export function openUrl( browser, error, }; + } catch (ex: unknown) { + throw new Error( + JSON.stringify( + { + message: "Failed to open URL", + url: urlString, + error: fromError(ex).toString(), + }, + null, + 2 + ) + ); + } } export function createBrowserConfig( diff --git a/packages/finicky-ui/src/App.svelte b/packages/finicky-ui/src/App.svelte index dcf78cc..7a81af3 100644 --- a/packages/finicky-ui/src/App.svelte +++ b/packages/finicky-ui/src/App.svelte @@ -1,12 +1,15 @@ - -
-
- - - Finicky icon - -

{version}

-
- - - -
-

- Created by John Sterling -

-

- Icon designed by @uetchy -

-
-
- - diff --git a/packages/finicky-ui/src/components/LogContent.svelte b/packages/finicky-ui/src/components/LogContent.svelte index 1add8d7..421e5f6 100644 --- a/packages/finicky-ui/src/components/LogContent.svelte +++ b/packages/finicky-ui/src/components/LogContent.svelte @@ -35,11 +35,11 @@ .log-content { list-style: none; margin: 0; - padding: 12px; + padding: 0; overflow-y: auto; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - line-height: 1.4; + line-height: 1.5; color: var(--text-primary); flex: 1; overflow: auto; @@ -49,25 +49,22 @@ display: flex; justify-content: flex-start; gap: 1em; - margin-bottom: 4px; + margin-bottom: 8px; align-items: flex-start; } .log-time { color: var(--text-secondary); white-space: nowrap; - font-size: 0.9em; - } - - .log-level-icon { - width: 1.5em; - text-align: center; + font-size: 0.85em; + opacity: 0.8; } .log-message { flex-grow: 1; white-space: pre-wrap; word-break: break-word; + font-size: 0.95em; } .log-level-error { diff --git a/packages/finicky-ui/src/components/LogViewer.svelte b/packages/finicky-ui/src/components/LogViewer.svelte deleted file mode 100644 index 8f6700c..0000000 --- a/packages/finicky-ui/src/components/LogViewer.svelte +++ /dev/null @@ -1,141 +0,0 @@ - - -
-
-

Logs

-
- - - -
-
- - -
- - diff --git a/packages/finicky-ui/src/components/PageContainer.svelte b/packages/finicky-ui/src/components/PageContainer.svelte new file mode 100644 index 0000000..b1746e5 --- /dev/null +++ b/packages/finicky-ui/src/components/PageContainer.svelte @@ -0,0 +1,62 @@ + + +
+ {#if title} +
+

{title}

+ {#if description} +

{description}

+ {/if} +
+ {/if} + {@render children()} +
+ + diff --git a/packages/finicky-ui/src/components/TabBar.svelte b/packages/finicky-ui/src/components/TabBar.svelte index 20dcaa9..1a752c2 100644 --- a/packages/finicky-ui/src/components/TabBar.svelte +++ b/packages/finicky-ui/src/components/TabBar.svelte @@ -1,33 +1,33 @@ diff --git a/packages/finicky-ui/src/components/TestUrl.svelte b/packages/finicky-ui/src/components/TestUrl.svelte deleted file mode 100644 index 2e3f38a..0000000 --- a/packages/finicky-ui/src/components/TestUrl.svelte +++ /dev/null @@ -1,220 +0,0 @@ - - -
-
-

Test URL Handler

-

- Test how Finicky will handle a URL based on your current configuration -

- -
- - {#if loading} -
Testing...
- {/if} -
- - {#if result} -
-

Result

-
-
- URL: - {result.url} -
-
- Browser: - {result.browser} -
-
- Open in Background: - {result.openInBackground ? "Yes" : "No"} -
- {#if result.profile} -
- Profile: - {result.profile} -
- {/if} -
-
- {:else if testUrl.trim() && !isValidUrl(testUrl)} -
- Enter a valid URL to see how Finicky will handle it -
- {/if} -
-
- - diff --git a/packages/finicky-ui/src/components/ToastContainer.svelte b/packages/finicky-ui/src/components/ToastContainer.svelte new file mode 100644 index 0000000..bf8c0f5 --- /dev/null +++ b/packages/finicky-ui/src/components/ToastContainer.svelte @@ -0,0 +1,170 @@ + + +
+ {#each toasts as t (t.id)} + + {/each} +
+ + diff --git a/packages/finicky-ui/src/components/Toggle.svelte b/packages/finicky-ui/src/components/Toggle.svelte new file mode 100644 index 0000000..e69de29 diff --git a/packages/finicky-ui/src/components/icons/External.svelte b/packages/finicky-ui/src/components/icons/External.svelte new file mode 100644 index 0000000..f029166 --- /dev/null +++ b/packages/finicky-ui/src/components/icons/External.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/finicky-ui/src/components/icons/Info.svelte b/packages/finicky-ui/src/components/icons/Info.svelte new file mode 100644 index 0000000..51fa882 --- /dev/null +++ b/packages/finicky-ui/src/components/icons/Info.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/finicky-ui/src/components/icons/Link.svelte b/packages/finicky-ui/src/components/icons/Link.svelte new file mode 100644 index 0000000..37eb36f --- /dev/null +++ b/packages/finicky-ui/src/components/icons/Link.svelte @@ -0,0 +1,14 @@ + + + + diff --git a/packages/finicky-ui/src/components/icons/General.svelte b/packages/finicky-ui/src/components/icons/Preferences.svelte similarity index 100% rename from packages/finicky-ui/src/components/icons/General.svelte rename to packages/finicky-ui/src/components/icons/Preferences.svelte diff --git a/packages/finicky-ui/src/components/icons/Spinner.svelte b/packages/finicky-ui/src/components/icons/Spinner.svelte new file mode 100644 index 0000000..8beb07c --- /dev/null +++ b/packages/finicky-ui/src/components/icons/Spinner.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/finicky-ui/src/lib/testUrlStore.ts b/packages/finicky-ui/src/lib/testUrlStore.ts new file mode 100644 index 0000000..5c974e3 --- /dev/null +++ b/packages/finicky-ui/src/lib/testUrlStore.ts @@ -0,0 +1,10 @@ +import { writable } from 'svelte/store'; + +export interface TestUrlResult { + browser: string; + url: string; + openInBackground: boolean; + profile?: string; +} + +export const testUrlResult = writable(null); diff --git a/packages/finicky-ui/src/lib/toast.ts b/packages/finicky-ui/src/lib/toast.ts new file mode 100644 index 0000000..2e43627 --- /dev/null +++ b/packages/finicky-ui/src/lib/toast.ts @@ -0,0 +1,105 @@ +import { writable } from "svelte/store"; + +export type ToastType = "success" | "error" | "info" | "warning"; + +export interface Toast { + id: string; + message: string; + extra?: string; + type: ToastType; + duration?: number; + timeoutId?: number; + key?: number; // Used to force animation restart +} + +const MAX_TOASTS = 3; +const DEFAULT_DURATION = 3000; + +function createToastStore() { + const { subscribe, update } = writable([]); + + function show(message: string, type: ToastType = "info", extra?: string, duration = DEFAULT_DURATION) { + let id: string; + let timeoutId: number; + + update((toasts) => { + // Check if toast with same message and type already exists + const existing = toasts.find((t) => t.message === message && t.type === type); + + if (existing) { + // Clear existing timeout + if (existing.timeoutId) { + clearTimeout(existing.timeoutId); + } + id = existing.id; + + // Reset timeout + timeoutId = setTimeout(() => { + remove(id); + }, duration) as unknown as number; + + // Update the existing toast with new timeout and key to force animation restart + return toasts.map((t) => + t.id === existing.id ? { ...t, duration, timeoutId, key: Date.now() } : t + ); + } else { + // Create new toast + id = `${Date.now()}-${Math.random()}`; + + timeoutId = setTimeout(() => { + remove(id); + }, duration) as unknown as number; + + const newToast: Toast = { id, message, extra, type, duration, timeoutId, key: Date.now() }; + const updatedToasts = [...toasts, newToast]; + + // Limit max toasts (remove oldest) + if (updatedToasts.length > MAX_TOASTS) { + const removed = updatedToasts[0]; + if (removed.timeoutId) { + clearTimeout(removed.timeoutId); + } + return updatedToasts.slice(-MAX_TOASTS); + } + + return updatedToasts; + } + }); + + return id!; + } + + function remove(id: string) { + update((toasts) => { + const toast = toasts.find((t) => t.id === id); + if (toast?.timeoutId) { + clearTimeout(toast.timeoutId); + } + return toasts.filter((t) => t.id !== id); + }); + } + + function clear() { + update((toasts) => { + toasts.forEach((t) => { + if (t.timeoutId) { + clearTimeout(t.timeoutId); + } + }); + return []; + }); + } + + return { + subscribe, + show, + success: (message: string, duration?: number) => show(message, "success", duration), + error: (message: string, error: string, duration?: number) => show(message, "error", error, duration), + info: (message: string, duration?: number) => show(message, "info", duration), + warning: (message: string, duration?: number) => show(message, "warning", duration), + remove, + clear, + }; +} + +export const toast = createToastStore(); diff --git a/packages/finicky-ui/src/pages/About.svelte b/packages/finicky-ui/src/pages/About.svelte new file mode 100644 index 0000000..92c41da --- /dev/null +++ b/packages/finicky-ui/src/pages/About.svelte @@ -0,0 +1,168 @@ + + + +
+ +
+
+ + + Finicky icon + +

Version {version}

+
+ +
+

About

+

Finicky is a macOS application that lets you set up rules to decide which browser to open for every link.

+ + View on GitHub + +
+ +
+

Credits

+

Created by John Sterling

+

Icon designed by @uetchy

+

View all contributors

+
+ +
+

Support Development

+

If you find Finicky useful, consider .

+
+
+
+ + diff --git a/packages/finicky-ui/src/pages/LogViewer.svelte b/packages/finicky-ui/src/pages/LogViewer.svelte new file mode 100644 index 0000000..1f0e82e --- /dev/null +++ b/packages/finicky-ui/src/pages/LogViewer.svelte @@ -0,0 +1,160 @@ + + + +
+

Logs

+
+ + + +
+
+ + +
+ + diff --git a/packages/finicky-ui/src/components/StartPage.svelte b/packages/finicky-ui/src/pages/StartPage.svelte similarity index 57% rename from packages/finicky-ui/src/components/StartPage.svelte rename to packages/finicky-ui/src/pages/StartPage.svelte index 0605af3..e93c8f3 100644 --- a/packages/finicky-ui/src/components/StartPage.svelte +++ b/packages/finicky-ui/src/pages/StartPage.svelte @@ -1,6 +1,8 @@ - -
+ {#if !hasConfig} -
-

No Configuration Found

-

Create a configuration file to customize your browser behavior.
- - Learn how to get started - -

+ {/if} - {#if hasConfig && (config.keepRunning !== undefined || config.hideIcon !== undefined || config.logRequests !== undefined || config.checkForUpdate !== undefined)} -
-

Configuration

-

Current settings from your configuration file

-
-
-
-
- Keep Running - App stays open after handling links -
- +
+
+
+
+
+ Keep running + App stays open in the background
+
-
-
-
- Hide Icon - Hide menu bar icon -
- +
+
+
+
+ Hide icon + Hide menu bar icon
+
-
-
-
- Log Requests - Log all URL handling to file -
- +
+
+
+
+ Log requests + Log all URL handling to file
+
-
-
-
- Check For Updates - Automatically check for new versions -
- +
+
+
+
+ Check for updates + Automatically check for new versions
+
- {/if} - - +
{#if numErrors > 0}
@@ -122,21 +139,9 @@ {/if} - -
- + diff --git a/packages/finicky-ui/src/pages/TestUrl.svelte b/packages/finicky-ui/src/pages/TestUrl.svelte new file mode 100644 index 0000000..6c6bf27 --- /dev/null +++ b/packages/finicky-ui/src/pages/TestUrl.svelte @@ -0,0 +1,299 @@ + + + +
+
+ + +
+ + {#if $testUrlResult} +
+
+

Result

+
+
+
+ Browser + {$testUrlResult.browser} +
+
+ Profile + {$testUrlResult.profile || "N/A"} +
+ {#if typeof $testUrlResult.openInBackground === "boolean"} +
+ Open in background + {$testUrlResult.openInBackground ? "Yes" : "No"} +
+ {/if} +
+ Final URL + {$testUrlResult.url} +
+
+
+ {:else if testUrl.trim() && !isValidUrl(testUrl)} +
+ + Enter a valid URL to see how Finicky will handle it +
+ {:else if !testUrl.trim()} +
+ +

Enter a URL above to test your configuration

+
+ {/if} +
+
+ + diff --git a/packages/finicky-ui/src/types.ts b/packages/finicky-ui/src/types.ts index 6ece014..3325fdd 100644 --- a/packages/finicky-ui/src/types.ts +++ b/packages/finicky-ui/src/types.ts @@ -34,7 +34,7 @@ export interface ConfigOptions { keepRunning: boolean; hideIcon: boolean; logRequests: boolean; - checkForUpdate: boolean; + checkForUpdates: boolean; } export interface ConfigInfo { @@ -42,8 +42,10 @@ export interface ConfigInfo { handlers?: number; rewrites?: number; defaultBrowser?: string; - keepRunning?: boolean; - hideIcon?: boolean; - logRequests?: boolean; - checkForUpdate?: boolean; + options?: { + keepRunning?: boolean; + hideIcon?: boolean; + logRequests?: boolean; + checkForUpdates?: boolean; + }; }