Skip to content
Merged
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
6 changes: 6 additions & 0 deletions apps/finicky/src/config/configfiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
16 changes: 0 additions & 16 deletions apps/finicky/src/config/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 5 additions & 5 deletions apps/finicky/src/go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
)
8 changes: 8 additions & 0 deletions apps/finicky/src/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand All @@ -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=
149 changes: 100 additions & 49 deletions apps/finicky/src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -229,32 +236,21 @@ func handleRuntimeError(err error) {
go QueueWindowDisplay(1)
}

func getKeepRunning() bool {
if vm == 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
}

keepRunning, err := vm.Runtime().RunString("finickyConfigAPI.getOption('keepRunning', finalConfig, true)")
if err != nil {
return false
}
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
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down Expand Up @@ -371,6 +410,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 {
Expand Down Expand Up @@ -417,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)
}
Expand All @@ -434,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 {
Expand All @@ -451,22 +498,26 @@ func setupVM(cfw *config.ConfigFileWatcher, embeddedFS embed.FS, namespace strin
DefaultBrowser: currentConfigState.DefaultBrowser,
ConfigPath: configPath,
}

window.SendMessageToWebView("config", map[string]interface{}{
"handlers": configInfo.Handlers,
"rewrites": configInfo.Rewrites,
"defaultBrowser": configInfo.DefaultBrowser,
"configPath": configInfo.ConfigPath,
})
} else if configInfo != nil {
window.SendMessageToWebView("config", map[string]interface{}{
"handlers": 0,
"rewrites": 0,
"defaultBrowser": "",
"configPath": configInfo.ConfigPath,
})
}

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
}

Expand Down
1 change: 1 addition & 0 deletions apps/finicky/src/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSApplicationDelegate>
Expand Down
23 changes: 22 additions & 1 deletion apps/finicky/src/main.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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];
Expand Down
Loading
Loading