package desktop import ( "fmt" "io/fs" "log" "net" "net/http" "os" "os/exec" "os/signal" "strconv" "strings" "syscall" "github.com/muyue/muyue/internal/api" "github.com/muyue/muyue/internal/config" "github.com/muyue/muyue/internal/version" "github.com/muyue/muyue/web" ) type options struct { port int noOpen bool } type option func(*options) func withPort(port int) option { return func(o *options) { o.port = port } } func withNoOpen(noOpen bool) option { return func(o *options) { o.noOpen = noOpen } } func parseFlags(args []string) []option { var opts []option for _, arg := range args { switch { case arg == "--no-open": opts = append(opts, withNoOpen(true)) case strings.HasPrefix(arg, "--port="): if p, err := strconv.Atoi(strings.TrimPrefix(arg, "--port=")); err == nil { opts = append(opts, withPort(p)) } case arg == "--port": // handled as prefix case } } return opts } func Run(cfg *config.MuyueConfig, args []string) error { o := options{} for _, opt := range parseFlags(args) { opt(&o) } log.Printf("%s Desktop v%s", version.Name, version.Version) srv := api.NewServer(cfg) frontendFS, err := fs.Sub(web.Assets, "dist") if err != nil { return fmt.Errorf("frontend assets: %w", err) } mux := http.NewServeMux() mux.Handle("/api/", srv) mux.Handle("/", spaHandler(http.FileServer(http.FS(frontendFS)))) addr := fmt.Sprintf("127.0.0.1:%d", o.port) listener, err := net.Listen("tcp", addr) if err != nil { return fmt.Errorf("bind %s: %w", addr, err) } port := listener.Addr().(*net.TCPAddr).Port go func() { if err := http.Serve(listener, mux); err != nil { log.Fatalf("Server error: %v", err) } }() url := fmt.Sprintf("http://127.0.0.1:%d", port) log.Printf("Listening on %s", url) if !o.noOpen { openBrowser(url) log.Printf("Opened browser") } quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutting down...") return nil } func spaHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path if path != "/" && !strings.Contains(path, ".") { r.URL.Path = "/" } next.ServeHTTP(w, r) }) } func openBrowser(url string) { var cmd *exec.Cmd switch { case exists("xdg-open"): cmd = exec.Command("xdg-open", url) case exists("open"): cmd = exec.Command("open", url) case exists("cmd"): cmd = exec.Command("cmd", "/c", "start", url) default: fmt.Printf("Open manually: %s\n", url) return } _ = cmd.Start() } func exists(name string) bool { _, err := exec.LookPath(name) return err == nil }