package gui import ( "context" "encoding/json" "errors" "fmt" "log" "net" "net/http" "os/exec" "runtime" "strings" "time" "lazy-update-manager/internal/config" "lazy-update-manager/internal/updater" ) type Server struct { configPath string } type StatusResponse struct { Summary string `json:"summary"` Total int `json:"total"` Packages []updater.Package `json:"packages"` Warnings []string `json:"warnings"` Settings config.Config `json:"settings"` } func Run(configPath string, openBrowser bool) error { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return err } server := &Server{configPath: configPath} mux := http.NewServeMux() mux.HandleFunc("/", server.index) mux.HandleFunc("/app.css", server.css) mux.HandleFunc("/app.js", server.js) mux.HandleFunc("/api/status", server.status) mux.HandleFunc("/api/settings", server.settings) mux.HandleFunc("/api/install", server.install) url := "http://" + listener.Addr().String() fmt.Println("LazyUpdateManager GUI:", url) if openBrowser { go func() { time.Sleep(250 * time.Millisecond) if err := openURL(url); err != nil { log.Printf("open browser: %v", err) } }() } return http.Serve(listener, mux) } func (s *Server) status(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } cfg, err := config.Load(s.configPath) if err != nil { writeError(w, err, http.StatusInternalServerError) return } ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) defer cancel() result, err := updater.CheckWithOptions(ctx, updater.Options{CheckAUR: cfg.CheckAUR}) if err != nil { writeError(w, err, http.StatusInternalServerError) return } packages := result.Packages if packages == nil { packages = []updater.Package{} } warnings := result.Warnings if warnings == nil { warnings = []string{} } writeJSON(w, StatusResponse{ Summary: result.Summary(), Total: result.Total(), Packages: packages, Warnings: warnings, Settings: cfg, }) } func (s *Server) settings(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: cfg, err := config.Load(s.configPath) if err != nil { writeError(w, err, http.StatusInternalServerError) return } writeJSON(w, cfg) case http.MethodPut: var cfg config.Config if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { writeError(w, err, http.StatusBadRequest) return } if err := config.Save(s.configPath, cfg); err != nil { writeError(w, err, http.StatusInternalServerError) return } writeJSON(w, cfg) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } func (s *Server) install(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } cfg, err := config.Load(s.configPath) if err != nil { writeError(w, err, http.StatusInternalServerError) return } name, args, err := terminalCommand(cfg) if err != nil { writeError(w, err, http.StatusBadRequest) return } cmd := exec.Command(name, args...) if err := cmd.Start(); err != nil { writeError(w, err, http.StatusInternalServerError) return } writeJSON(w, map[string]string{"status": "started"}) } func terminalCommand(cfg config.Config) (string, []string, error) { updateCommand := "lazy-update-manager update; printf '\\nDone. Press enter to close... '; read _" if helper := updater.AURHelper(); helper != "" && cfg.CheckAUR { updateCommand = helper + " -Syu; printf '\\nDone. Press enter to close... '; read _" } candidates := []string{} if cfg.Terminal != "" && cfg.Terminal != "auto" { candidates = append(candidates, cfg.Terminal) } candidates = append(candidates, "foot", "kitty", "alacritty", "wezterm", "ghostty", "konsole", "xterm") for _, terminal := range candidates { if _, err := exec.LookPath(terminal); err != nil { continue } switch terminal { case "foot", "kitty", "ghostty": return terminal, []string{"sh", "-lc", updateCommand}, nil case "alacritty": return terminal, []string{"-e", "sh", "-lc", updateCommand}, nil case "wezterm": return terminal, []string{"start", "--", "sh", "-lc", updateCommand}, nil case "konsole": return terminal, []string{"-e", "sh", "-lc", updateCommand}, nil case "xterm": return terminal, []string{"-e", "sh", "-lc", updateCommand}, nil default: return terminal, []string{"-e", "sh", "-lc", updateCommand}, nil } } return "", nil, errors.New("no supported terminal found; set one in settings") } func openURL(url string) error { switch runtime.GOOS { case "linux": return exec.Command("xdg-open", url).Start() case "darwin": return exec.Command("open", url).Start() default: return exec.Command("xdg-open", url).Start() } } func writeJSON(w http.ResponseWriter, value any) { securityHeaders(w) w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(value); err != nil { log.Printf("write json: %v", err) } } func writeError(w http.ResponseWriter, err error, status int) { securityHeaders(w) w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) } func securityHeaders(w http.ResponseWriter) { w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'") w.Header().Set("X-Content-Type-Options", "nosniff") } func (s *Server) index(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } securityHeaders(w) w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprint(w, strings.TrimSpace(indexHTML)) } func (s *Server) css(w http.ResponseWriter, r *http.Request) { securityHeaders(w) w.Header().Set("Content-Type", "text/css; charset=utf-8") fmt.Fprint(w, appCSS) } func (s *Server) js(w http.ResponseWriter, r *http.Request) { securityHeaders(w) w.Header().Set("Content-Type", "text/javascript; charset=utf-8") fmt.Fprint(w, appJS) }