2026-07-22 14:51:11 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-07-22 15:19:11 +08:00
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2026-07-22 15:13:55 +08:00
|
|
|
"log/slog"
|
2026-07-22 14:51:11 +08:00
|
|
|
"os"
|
2026-07-22 15:19:11 +08:00
|
|
|
"strings"
|
2026-07-22 14:51:11 +08:00
|
|
|
|
2026-07-22 15:17:08 +08:00
|
|
|
"chub/internal/platform/config"
|
2026-07-22 14:51:11 +08:00
|
|
|
"chub/internal/platform/logging"
|
2026-07-22 15:13:55 +08:00
|
|
|
"chub/internal/ui"
|
|
|
|
|
"gioui.org/app"
|
|
|
|
|
"gioui.org/layout"
|
|
|
|
|
"gioui.org/op"
|
|
|
|
|
"gioui.org/unit"
|
|
|
|
|
"gioui.org/widget/material"
|
2026-07-22 14:51:11 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const version = "0.1.0-dev"
|
|
|
|
|
|
|
|
|
|
func main() {
|
2026-07-22 15:19:11 +08:00
|
|
|
if len(os.Args) > 1 {
|
|
|
|
|
os.Exit(runCLI(os.Args[1:], os.Stdout, os.Stderr))
|
|
|
|
|
}
|
2026-07-22 14:51:11 +08:00
|
|
|
ctx := context.Background()
|
|
|
|
|
logger := logging.New(os.Stderr)
|
|
|
|
|
logger.InfoContext(ctx, "chub starting", "version", version)
|
2026-07-22 15:13:55 +08:00
|
|
|
go runWindow(logger)
|
|
|
|
|
app.Main()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 15:19:11 +08:00
|
|
|
type cliError struct {
|
|
|
|
|
Code string `json:"code"`
|
|
|
|
|
Message string `json:"message"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runCLI(args []string, out, errout io.Writer) int {
|
|
|
|
|
if len(args) == 0 {
|
|
|
|
|
return 2
|
|
|
|
|
}
|
|
|
|
|
command := args[0]
|
|
|
|
|
if command == "--help" || command == "-h" {
|
|
|
|
|
fmt.Fprintln(out, "chub [start|list|stop|restart|events]")
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
path, err := config.DefaultPath()
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeCLIError(errout, "system_error", err.Error())
|
|
|
|
|
return 6
|
|
|
|
|
}
|
|
|
|
|
store, err := config.New(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeCLIError(errout, "system_error", err.Error())
|
|
|
|
|
return 6
|
|
|
|
|
}
|
|
|
|
|
switch command {
|
|
|
|
|
case "list":
|
|
|
|
|
value, err := store.Load()
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeCLIError(errout, "system_error", err.Error())
|
|
|
|
|
return 6
|
|
|
|
|
}
|
|
|
|
|
return writeJSON(out, map[string]any{"instances": value.Instances})
|
|
|
|
|
case "events":
|
|
|
|
|
// T-203 exposes a local JSON-lines snapshot; the live in-process bus is
|
|
|
|
|
// consumed by the UI and will be connected to a loopback transport later.
|
|
|
|
|
value, err := store.Load()
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeCLIError(errout, "system_error", err.Error())
|
|
|
|
|
return 6
|
|
|
|
|
}
|
|
|
|
|
return writeJSON(out, map[string]any{"event": "browser.snapshot", "instances": value.Instances})
|
|
|
|
|
case "start", "stop", "restart":
|
|
|
|
|
writeCLIError(errout, "service_unavailable", strings.TrimSpace(command)+" requires the BrowserManager adapter")
|
|
|
|
|
return 6
|
|
|
|
|
default:
|
|
|
|
|
writeCLIError(errout, "invalid_argument", "unknown command: "+command)
|
|
|
|
|
return 2
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeJSON(out io.Writer, value any) int {
|
|
|
|
|
if err := json.NewEncoder(out).Encode(value); err != nil {
|
|
|
|
|
return 6
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeCLIError(out io.Writer, code, message string) {
|
|
|
|
|
_ = json.NewEncoder(out).Encode(cliError{Code: code, Message: message})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 15:13:55 +08:00
|
|
|
func runWindow(logger *slog.Logger) {
|
|
|
|
|
window := new(app.Window)
|
|
|
|
|
window.Option(app.Title("Chub 浏览器管理"), app.Size(unit.Dp(1100), unit.Dp(720)))
|
|
|
|
|
theme := material.NewTheme()
|
|
|
|
|
shell := ui.NewShell(theme)
|
2026-07-22 15:17:08 +08:00
|
|
|
if path, err := config.DefaultPath(); err == nil {
|
|
|
|
|
if store, err := config.New(path); err == nil {
|
|
|
|
|
if saved, err := store.Load(); err == nil {
|
|
|
|
|
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{Name: item.Name, Browser: string(item.Launch.Kind), Profile: item.Launch.ProfileDirectory, 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)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-22 15:13:55 +08:00
|
|
|
var ops op.Ops
|
|
|
|
|
for {
|
|
|
|
|
switch event := window.Event().(type) {
|
|
|
|
|
case app.DestroyEvent:
|
|
|
|
|
if event.Err != nil {
|
|
|
|
|
logger.ErrorContext(context.Background(), "chub window closed", "error", event.Err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
case app.FrameEvent:
|
|
|
|
|
gtx := app.NewContext(&ops, event)
|
|
|
|
|
shell.Layout(layout.Context(gtx))
|
|
|
|
|
event.Frame(gtx.Ops)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-22 14:51:11 +08:00
|
|
|
}
|
2026-07-22 15:17:08 +08:00
|
|
|
|
|
|
|
|
func saveSettings(logger *slog.Logger, store *config.Store, value config.File) {
|
|
|
|
|
err := store.Save(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Error("save settings", "error", err)
|
|
|
|
|
}
|
|
|
|
|
}
|