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
7 changes: 5 additions & 2 deletions docs/guide/menubar.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Or run from source:
go run -tags=safe ./cmd/dockprox-menubar
```

The proxy auto-starts when the app launches. The tray icon reflects the running / stopped state.
The proxy auto-starts when the app launches. The tray icon reflects the running / stopped state, and dims briefly while a restart is in progress.

## Flags

Expand Down Expand Up @@ -72,14 +72,17 @@ cache dir: `~/Library/Caches/org.foomo.dockprox/dockprox.log`.
| **Version** (top item) | Shows the running app version. Click to open the GitHub releases page. |
| **Start** | Start the in-process proxy with the resolved config. |
| **Stop** | Stop the running proxy. |
| **Restart** | Stop, reload the config from disk, and start again. |
| **Restart** | Stop, reload the config from disk, and start again. Disabled β€” and the tray icon dims β€” while a restart is in flight. |
| *(tunnel rows)* | One row per SSH upstream with a `socks5Listen`, e.g. `β—‰ bastion: 127.0.0.1:1080`. Click a listening tunnel (`β—‰`) to stop it, or a stopped one (`β—‹`) to start it, independently of the main proxy. |
| **Start at Login** | Toggle launching the app automatically at login β€” needed since dockprox must be running for a system-wide proxy setup to reach anything. |
| **Reveal logs in Finder** | Open the log file (see [Logging](#logging)) in Finder. |
| **Reveal config in Finder** | Open the resolved config file in Finder. |
| **Quit** | Stop the proxy and exit the app. |

Edit the resolved config file with any editor and use **Restart** to apply changes β€” there is no live-reload.

Tunnel rows are only shown while the main proxy is running, and are disabled during a restart.

## See also

- [Configuration](./configuration.md)
66 changes: 57 additions & 9 deletions internal/menubar/tray.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,27 @@ import (
"context"
"fmt"
"runtime"
"sync/atomic"
"time"

"github.com/charmbracelet/log"
"github.com/foomo/dockprox/pkg/sshclient"
"github.com/wailsapp/wails/v3/pkg/application"
)

// restartBusyFloor is the minimum time the tray shows the disabled icon
// after a Restart click, so the feedback is visible even though the
// underlying Stop+Start cycle typically completes in microseconds.
const restartBusyFloor = 500 * time.Millisecond

// Tray binds a ProxyController to a Wails v3 system tray.
type Tray struct {
app *application.App
systray *application.SystemTray
ctrl *ProxyController
logger *log.Logger
logPath string
busy atomic.Bool
}

// NewTray creates the system tray and wires it to the controller.
Expand Down Expand Up @@ -52,7 +60,7 @@ func NewTray(app *application.App, ctrl *ProxyController, logger *log.Logger, lo

func (t *Tray) applyIcon(s State) {
var icon []byte
if s == StateRunning {
if s == StateRunning && !t.busy.Load() {
icon = iconRunning
} else {
icon = iconStopped
Expand Down Expand Up @@ -84,20 +92,41 @@ func (t *Tray) rebuildMenu() {
menu.Add("β—‰ " + snap.ListenAddr).SetEnabled(false)
}

// Tunnels: read-only status list, one glyph+name row each.
// Tunnels: one glyph+name row each, click to start/stop that tunnel.
if len(snap.Tunnels) > 0 {
menu.AddSeparator()

for _, ts := range snap.Tunnels {
name := ts.Name
listening := ts.State == TunnelListening

glyph := "β—‹"
addr := "-"

if ts.ConnState == sshclient.ConnConnected {
glyph = "β—‰"
if listening {
addr = ts.Addr

if ts.ConnState == sshclient.ConnConnected {
glyph = "β—‰"
}
}

menu.Add(fmt.Sprintf("%s %s: %s", glyph, ts.Name, addr)).SetEnabled(false)
item := menu.Add(fmt.Sprintf("%s %s: %s", glyph, name, addr))
item.SetEnabled(snap.State == StateRunning && !t.busy.Load())

if listening {
item.OnClick(func(_ *application.Context) {
if err := t.ctrl.StopTunnel(name); err != nil {
t.logger.Warn("stop tunnel", "name", name, "err", err)
}
})
} else {
item.OnClick(func(_ *application.Context) {
if err := t.ctrl.StartTunnel(name); err != nil {
t.logger.Warn("start tunnel", "name", name, "err", err)
}
})
}
}
}

Expand All @@ -117,10 +146,29 @@ func (t *Tray) rebuildMenu() {
}

menu.Add("β†Ί Restart").OnClick(func(_ *application.Context) {
if err := t.ctrl.Restart(); err != nil {
t.logger.Warn("restart", "err", err)
}
}).SetEnabled(snap.State == StateRunning)
t.busy.Store(true)
t.applyIcon(snap.State)
t.rebuildMenu()

go func() {
start := time.Now()

if err := t.ctrl.Restart(); err != nil {
t.logger.Warn("restart", "err", err)
}

if remaining := restartBusyFloor - time.Since(start); remaining > 0 {
time.Sleep(remaining)
}

t.busy.Store(false)

application.InvokeAsync(func() {
t.applyIcon(t.ctrl.Snapshot().State)
t.rebuildMenu()
})
}()
}).SetEnabled(snap.State == StateRunning && !t.busy.Load())

autostartEnabled, err := t.app.Autostart.IsEnabled()
if err != nil {
Expand Down