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
27 changes: 24 additions & 3 deletions gtk/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
package gtk

import (
"context"
"fmt"
"net/url"
"os"
"strings"

appconfig "github.com/alyraffauf/switchyard/internal/config"
"github.com/alyraffauf/switchyard/internal/host"
"github.com/alyraffauf/switchyard/internal/routing"
"github.com/alyraffauf/switchyard/internal/startup"
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gio/v2"
Expand All @@ -21,26 +24,44 @@ import (
// Run starts the Switchyard GTK application and blocks until it exits.
func Run() {
app := adw.NewApplication(getAppID(), gio.ApplicationHandlesOpen)
mode := startup.LaunchMode{}

app.AddMainOption("native-host", 0, glib.OptionFlagNone, glib.OptionArgNone,
"Run as native-messaging host (invoked by browsers)", "")
app.AddMainOption("background", 0, glib.OptionFlagNone, glib.OptionArgNone,
"Start without opening a window", "")

app.ConnectHandleLocalOptions(func(options *glib.VariantDict) int {
if options.Contains("native-host") {
runNativeMessagingHost()
return 0
}
if options.Contains("background") {
mode.Background = true
if err := app.Register(context.Background()); err != nil {
fmt.Fprintf(os.Stderr, "Error: could not start Switchyard in the background: %v\n", err)
return 1
}
if mode.ShouldHandleLocally(app.IsRemote()) {
return 0
}
}
return -1
})

app.ConnectActivate(func() {
cfg, _ := appconfig.Load(appconfig.Path())
if cfg.StayAlive {
shouldHold := mode.ShouldHold(cfg.StayAlive)
shouldShowWindow := mode.ShouldShowWindow()
mode.CompleteActivation()

if shouldHold {
app.Hold()
}
setupApp(cfg)
browsers := detectBrowsers()
showSettingsWindow(app, browsers, cfg)
if shouldShowWindow {
showSettingsWindow(app, detectBrowsers(), cfg)
}
})

app.ConnectOpen(func(files []gio.Filer, hint string) {
Expand Down
33 changes: 33 additions & 0 deletions internal/startup/mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-3.0-or-later

// Package startup describes how a Switchyard invocation should start.
package startup

// LaunchMode contains invocation-specific startup behavior.
type LaunchMode struct {
Background bool
}

// ShouldHold reports whether the application should keep running without a
// window.
func (mode LaunchMode) ShouldHold(stayAlive bool) bool {
return mode.Background || stayAlive
}

// ShouldShowWindow reports whether activation should present the settings
// window.
func (mode LaunchMode) ShouldShowWindow() bool {
return !mode.Background
}

// CompleteActivation clears invocation-specific behavior so later activations
// forwarded to this process behave normally.
func (mode *LaunchMode) CompleteActivation() {
mode.Background = false
}

// ShouldHandleLocally reports whether this invocation can exit without
// activating an existing primary instance.
func (mode LaunchMode) ShouldHandleLocally(remote bool) bool {
return mode.Background && remote
}
46 changes: 46 additions & 0 deletions internal/startup/mode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package startup

import "testing"

func TestLaunchModeBackgroundStart(t *testing.T) {
mode := LaunchMode{Background: true}

if !mode.ShouldHold(false) {
t.Fatal("background start should hold the application even when stay_alive is disabled")
}
if mode.ShouldShowWindow() {
t.Fatal("background start should not show a window")
}
if !mode.ShouldHandleLocally(true) {
t.Fatal("background start should exit locally when another instance is already running")
}
}

func TestLaunchModeBackgroundOnlyAppliesToFirstActivation(t *testing.T) {
mode := LaunchMode{Background: true}

mode.CompleteActivation()

if !mode.ShouldShowWindow() {
t.Fatal("a later activation should show a window")
}
}

func TestLaunchModeNormalStart(t *testing.T) {
mode := LaunchMode{}

if mode.ShouldHold(false) {
t.Fatal("normal start should respect a disabled stay_alive setting")
}
if !mode.ShouldHold(true) {
t.Fatal("normal start should respect an enabled stay_alive setting")
}
if !mode.ShouldShowWindow() {
t.Fatal("normal start should show a window")
}
if mode.ShouldHandleLocally(true) {
t.Fatal("normal start should be forwarded to the primary instance")
}
}
16 changes: 15 additions & 1 deletion website/src/content/docs/using.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ order: 10

Switchyard is a powerful, rules-based browser launcher. Once it is set as your default browser, clicked links pass through Switchyard first. It can then open the link in a specific browser, ask you which browser to use, or rewrite the URL before opening it.

Use it when different parts of your life live in different browsers: work links in Chrome, personal links in Firefox, video links in Brave, or to add privacy-friendly redirects before anything opens.
Use it when different parts of your life live in different browsers: work links in Chrome, personal links in Firefox, video links in Brave, or to add privacy-friendly redirects before anything opens.

The basic flow is:

Expand All @@ -24,3 +24,17 @@ xdg-settings set default-web-browser io.github.alyraffauf.Switchyard.desktop
```

Or use your desktop environment's graphical settings to set Switchyard as the default browser.

## Start in the Background

To preload Switchyard without opening its settings window, add one of these commands to your desktop environment's startup applications:

```bash
# Flatpak installation
flatpak run io.github.alyraffauf.Switchyard --background

# Native installation
switchyard --background
```

The background option keeps Switchyard running for that session even when **Keep running in background** is disabled. If Switchyard is already running, the command exits without opening another window.