feat: wire gui instance start and delete confirmation
This commit is contained in:
+99
-2
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"chub/internal/domain"
|
||||
"chub/internal/platform/browser"
|
||||
@@ -106,6 +107,7 @@ func runWindow(logger *slog.Logger) {
|
||||
directoryPicker := files.NewDirectoryPicker()
|
||||
shell.OnChooseDirectory(directoryPicker.ChooseDirectory, window.Invalidate)
|
||||
discoverer := browser.NewDiscoverer()
|
||||
shell.OnStartInstance(instanceStarter{launcher: browser.NewOSLauncher(), resolver: discoverer}.Start, window.Invalidate)
|
||||
shell.OnPathSearch(func(ctx context.Context, field ui.PathField, current string) (string, error) {
|
||||
switch field {
|
||||
case ui.PathChromeExecutable:
|
||||
@@ -138,12 +140,16 @@ func runWindow(logger *slog.Logger) {
|
||||
shell.SetSettings(ui.SettingsState{ChromePath: saved.Settings.ChromePath, EdgePath: saved.Settings.EdgePath, DefaultDir: saved.Settings.DefaultDir, LogDir: saved.Settings.LogDir, CloseOnExit: saved.Settings.CloseOnExit})
|
||||
rows := make([]ui.InstanceRow, 0, len(saved.Instances))
|
||||
for _, item := range saved.Instances {
|
||||
rows = append(rows, ui.InstanceRow{ID: item.ID, Name: item.Name, Browser: string(item.Launch.Kind), UserDataDir: item.Launch.UserDataDir, Status: "已退出"})
|
||||
rows = append(rows, ui.InstanceRow{ID: item.ID, Name: item.Name, Browser: browserLabel(item.Launch.Kind), UserDataDir: item.Launch.UserDataDir, TargetURL: item.Launch.TargetURL, Status: "已退出"})
|
||||
}
|
||||
shell.SetInstances(rows)
|
||||
shell.OnSave(func(value ui.SettingsState) {
|
||||
saved.Settings = config.Settings{ChromePath: value.ChromePath, EdgePath: value.EdgePath, DefaultDir: value.DefaultDir, LogDir: value.LogDir, CloseOnExit: value.CloseOnExit}
|
||||
go saveSettings(logger, store, saved)
|
||||
go saveSettings(logger, store, configSnapshot(saved))
|
||||
})
|
||||
shell.OnInstancesChanged(func(rows []ui.InstanceRow) {
|
||||
saved.Instances = mergeInstanceConfig(saved.Instances, rows)
|
||||
go saveSettings(logger, store, configSnapshot(saved))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -170,3 +176,94 @@ func saveSettings(logger *slog.Logger, store *config.Store, value config.File) {
|
||||
logger.Error("save settings", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
type executableResolver interface {
|
||||
Resolve(context.Context, domain.BrowserKind, string) (string, error)
|
||||
}
|
||||
|
||||
type processLauncher interface {
|
||||
Start(context.Context, domain.LaunchSpec) (browser.ProcessHandle, error)
|
||||
}
|
||||
|
||||
type instanceStarter struct {
|
||||
launcher processLauncher
|
||||
resolver executableResolver
|
||||
}
|
||||
|
||||
func (s instanceStarter) Start(ctx context.Context, row ui.InstanceRow, settings ui.SettingsState) (int, error) {
|
||||
kind, err := browserKind(row.Browser)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if s.launcher == nil || s.resolver == nil {
|
||||
return 0, errors.New("浏览器启动服务尚未准备好")
|
||||
}
|
||||
configuredExecutable := settings.ChromePath
|
||||
if kind == domain.BrowserEdge {
|
||||
configuredExecutable = settings.EdgePath
|
||||
}
|
||||
executable, err := s.resolver.Resolve(ctx, kind, configuredExecutable)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("无法找到%s可执行文件:%w", browserLabel(kind), err)
|
||||
}
|
||||
handle, err := s.launcher.Start(ctx, domain.LaunchSpec{
|
||||
Kind: kind,
|
||||
Executable: executable,
|
||||
UserDataDir: row.UserDataDir,
|
||||
TargetURL: row.TargetURL,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return handle.PID(), nil
|
||||
}
|
||||
|
||||
func browserKind(value string) (domain.BrowserKind, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "chrome":
|
||||
return domain.BrowserChrome, nil
|
||||
case "edge":
|
||||
return domain.BrowserEdge, nil
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的浏览器类型:%s", value)
|
||||
}
|
||||
}
|
||||
|
||||
func browserLabel(kind domain.BrowserKind) string {
|
||||
if kind == domain.BrowserEdge {
|
||||
return "Edge"
|
||||
}
|
||||
return "Chrome"
|
||||
}
|
||||
|
||||
func mergeInstanceConfig(existing []config.Instance, rows []ui.InstanceRow) []config.Instance {
|
||||
byID := make(map[string]config.Instance, len(existing))
|
||||
for _, item := range existing {
|
||||
byID[item.ID] = item
|
||||
}
|
||||
updated := make([]config.Instance, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
kind, err := browserKind(row.Browser)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
item := byID[row.ID]
|
||||
item.ID = row.ID
|
||||
item.Name = row.Name
|
||||
item.Launch.Kind = kind
|
||||
item.Launch.UserDataDir = row.UserDataDir
|
||||
item.Launch.TargetURL = row.TargetURL
|
||||
item.UpdatedAt = time.Now()
|
||||
updated = append(updated, item)
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func configSnapshot(value config.File) config.File {
|
||||
clone := value
|
||||
clone.Instances = append([]config.Instance(nil), value.Instances...)
|
||||
for i := range clone.Instances {
|
||||
clone.Instances[i].Launch.ExtraArgs = append([]string(nil), clone.Instances[i].Launch.ExtraArgs...)
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
@@ -2,8 +2,14 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"chub/internal/domain"
|
||||
"chub/internal/platform/browser"
|
||||
"chub/internal/platform/config"
|
||||
"chub/internal/ui"
|
||||
)
|
||||
|
||||
func TestRunCLIRejectsUnknownCommandWithStableCode(t *testing.T) {
|
||||
@@ -12,3 +18,62 @@ func TestRunCLIRejectsUnknownCommandWithStableCode(t *testing.T) {
|
||||
t.Fatalf("code=%d output=%s", code, out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstanceStarterBuildsLaunchSpecFromInstanceAndSettings(t *testing.T) {
|
||||
launcher := &fakeProcessLauncher{handle: fakeProcessHandle{pid: 4242}}
|
||||
resolver := &fakeExecutableResolver{path: `C:\Browser\msedge.exe`}
|
||||
starter := instanceStarter{launcher: launcher, resolver: resolver}
|
||||
pid, err := starter.Start(context.Background(), ui.InstanceRow{ID: "edge-a", Browser: "Edge", UserDataDir: `C:\profiles\edge-a`}, ui.SettingsState{EdgePath: `C:\Configured\msedge.exe`})
|
||||
if err != nil || pid != 4242 {
|
||||
t.Fatalf("Start() = %d, %v", pid, err)
|
||||
}
|
||||
if resolver.kind != domain.BrowserEdge || resolver.configured != `C:\Configured\msedge.exe` {
|
||||
t.Fatalf("resolver input = %s, %q", resolver.kind, resolver.configured)
|
||||
}
|
||||
if launcher.spec.Kind != domain.BrowserEdge || launcher.spec.Executable != resolver.path || launcher.spec.TargetURL != "" {
|
||||
t.Fatalf("launch spec = %#v", launcher.spec)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeInstanceConfigPreservesExistingLaunchOptions(t *testing.T) {
|
||||
existing := []config.Instance{{ID: "one", Name: "旧名称", Launch: domain.LaunchSpec{Kind: domain.BrowserChrome, UserDataDir: `C:\profiles\old`, ProfileDirectory: "Default", ProxyServer: "https://proxy.local:8443", ExtraArgs: []string{"--window-size=1280,900"}}}}
|
||||
updated := mergeInstanceConfig(existing, []ui.InstanceRow{{ID: "one", Name: "新名称", Browser: "Edge", UserDataDir: `C:\profiles\new`, TargetURL: "https://example.com"}})
|
||||
if len(updated) != 1 {
|
||||
t.Fatalf("instances = %d, want 1", len(updated))
|
||||
}
|
||||
got := updated[0]
|
||||
if got.Name != "新名称" || got.Launch.Kind != domain.BrowserEdge || got.Launch.UserDataDir != `C:\profiles\new` || got.Launch.TargetURL != "https://example.com" {
|
||||
t.Fatalf("updated instance = %#v", got)
|
||||
}
|
||||
if got.Launch.ProfileDirectory != "Default" || got.Launch.ProxyServer != "https://proxy.local:8443" || len(got.Launch.ExtraArgs) != 1 {
|
||||
t.Fatalf("existing launch options were lost: %#v", got.Launch)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeExecutableResolver struct {
|
||||
path string
|
||||
kind domain.BrowserKind
|
||||
configured string
|
||||
}
|
||||
|
||||
func (r *fakeExecutableResolver) Resolve(_ context.Context, kind domain.BrowserKind, configured string) (string, error) {
|
||||
r.kind = kind
|
||||
r.configured = configured
|
||||
return r.path, nil
|
||||
}
|
||||
|
||||
type fakeProcessLauncher struct {
|
||||
spec domain.LaunchSpec
|
||||
handle browser.ProcessHandle
|
||||
}
|
||||
|
||||
func (l *fakeProcessLauncher) Start(_ context.Context, spec domain.LaunchSpec) (browser.ProcessHandle, error) {
|
||||
l.spec = spec
|
||||
return l.handle, nil
|
||||
}
|
||||
|
||||
type fakeProcessHandle struct{ pid int }
|
||||
|
||||
func (p fakeProcessHandle) PID() int { return p.pid }
|
||||
|
||||
func (fakeProcessHandle) Wait(context.Context) (int, error) { return 0, nil }
|
||||
|
||||
Reference in New Issue
Block a user