All checks were successful
Beta Release / beta (push) Successful in 37s
- Merge muyue + muyue-desktop into one binary (13MB) - `muyue` starts TUI, `muyue desktop` launches web UI in browser - Move frontend from cmd/muyue-desktop/frontend/ to web/ (standard Go layout) - Add web/embed.go with //go:embed all:dist for frontend assets - Add internal/desktop/ package (server, browser open, SPA routing, signals) - Split internal/api/api.go into server.go + handlers.go - Add internal/desktop/desktop.go with SPA fallback and --port/--no-open flags - Clean package.json: remove unused @xterm/xterm, switch to ESM - Fix vite.config.js proxy to use port 8095 for dev mode - Add Makefile targets: frontend, desktop, dev-desktop - Update all CI workflows: single binary build, web/ paths - Remove cmd/muyue-desktop/ entirely 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
132 lines
2.6 KiB
Go
132 lines
2.6 KiB
Go
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
|
|
}
|