package main import ( "context" "errors" "flag" "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "time" "lazy-update-manager/internal/config" "lazy-update-manager/internal/gui" "lazy-update-manager/internal/notify" "lazy-update-manager/internal/state" "lazy-update-manager/internal/updater" "lazy-update-manager/internal/version" ) func main() { os.Exit(run(os.Args[1:])) } func run(args []string) int { if len(args) == 0 { args = []string{"status"} } switch args[0] { case "status": return status() case "check": return check(args[1:]) case "notify": return notifyUpdates(args[1:]) case "gui": return runGUI(args[1:]) case "update": return update() case "version": fmt.Println(version.String()) return 0 case "help", "-h", "--help": usage() return 0 default: fmt.Fprintf(os.Stderr, "unknown command: %s\n\n", args[0]) usage() return 2 } } func status() int { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() cfg, err := config.Load(defaultConfigPath()) if err != nil { fmt.Fprintln(os.Stderr, err) return 1 } result, err := updater.CheckWithOptions(ctx, updater.Options{ CheckAUR: cfg.CheckAUR, IgnoredPackages: cfg.IgnoredPackages, }) if err != nil { fmt.Fprintln(os.Stderr, err) return 1 } printResult(result) return 0 } func check(args []string) int { fs := flag.NewFlagSet("check", flag.ContinueOnError) fs.SetOutput(os.Stderr) quiet := fs.Bool("quiet", false, "only print the number of available updates") if err := fs.Parse(args); err != nil { return 2 } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() cfg, err := config.Load(defaultConfigPath()) if err != nil { fmt.Fprintln(os.Stderr, err) return 1 } result, err := updater.CheckWithOptions(ctx, updater.Options{ CheckAUR: cfg.CheckAUR, IgnoredPackages: cfg.IgnoredPackages, }) if err != nil { fmt.Fprintln(os.Stderr, err) return 1 } if *quiet { fmt.Println(result.Total()) return 0 } printResult(result) return 0 } func notifyUpdates(args []string) int { fs := flag.NewFlagSet("notify", flag.ContinueOnError) fs.SetOutput(os.Stderr) force := fs.Bool("force", false, "send a notification even if the weekly reminder was already shown") if err := fs.Parse(args); err != nil { return 2 } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() cfg, err := config.Load(defaultConfigPath()) if err != nil { fmt.Fprintln(os.Stderr, err) return 1 } result, err := updater.CheckWithOptions(ctx, updater.Options{ CheckAUR: cfg.CheckAUR, IgnoredPackages: cfg.IgnoredPackages, }) if err != nil { fmt.Fprintln(os.Stderr, err) return 1 } if result.Total() == 0 { return 0 } if !cfg.NotificationsEnabled { return 0 } store, err := state.Load(defaultStatePath()) if err != nil { fmt.Fprintln(os.Stderr, err) return 1 } now := time.Now() reminderInterval := time.Duration(cfg.ReminderIntervalHours) * time.Hour if !*force && now.Sub(store.LastReminder) < reminderInterval { return 0 } message := result.Summary() if err := notify.Send("LazyUpdateManager", message); err != nil { fmt.Fprintln(os.Stderr, err) return 1 } store.LastReminder = now store.LastUpdateCount = result.Total() store.LastCheck = now store.LastSuccess = now store.LastSummary = result.Summary() if err := state.Save(defaultStatePath(), store); err != nil { fmt.Fprintln(os.Stderr, err) return 1 } return 0 } func runGUI(args []string) int { fs := flag.NewFlagSet("gui", flag.ContinueOnError) fs.SetOutput(os.Stderr) noOpen := fs.Bool("no-open", false, "start the GUI server without opening a browser") // Support positional "web" argument: `lazy-update-manager gui web` forceWeb := false if len(args) > 0 && args[0] == "web" { forceWeb = true args = args[1:] } if err := fs.Parse(args); err != nil { return 2 } cfg, err := config.Load(defaultConfigPath()) if err != nil { fmt.Fprintln(os.Stderr, err) return 1 } // If the user explicitly requested the web interface, skip Electron if forceWeb { if err := gui.Run(defaultConfigPath(), defaultStatePath(), !*noOpen); err != nil { fmt.Fprintln(os.Stderr, err) return 1 } return 0 } // Only try launching the Electron desktop when the caller did not request // "-no-open" (used by the Electron wrapper to start the backend). // This avoids the Electron<->backend launch loop. if !*noOpen && cfg.PreferElectron && runtime.GOOS == "linux" { if err := tryElectronApp(); err == nil { return 0 } // Fall through to browser if Electron fails } if err := gui.Run(defaultConfigPath(), defaultStatePath(), !*noOpen); err != nil { fmt.Fprintln(os.Stderr, err) return 1 } return 0 } func update() int { cfg, err := config.Load(defaultConfigPath()) if err != nil { fmt.Fprintln(os.Stderr, err) return 1 } helper := updater.AURHelper() if helper != "" && cfg.CheckAUR { return runInteractive(helper, "-Syu") } return runInteractive("sudo", "pacman", "-Syu") } func runInteractive(name string, args ...string) int { cmd := exec.Command(name, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) { return exitErr.ExitCode() } fmt.Fprintln(os.Stderr, err) return 1 } return 0 } func printResult(result updater.Result) { fmt.Println(result.Summary()) for _, item := range result.Packages { fmt.Printf("%-8s %s", item.Source, item.Name) if item.Current != "" || item.Available != "" { fmt.Printf(" %s -> %s", item.Current, item.Available) } fmt.Println() } } func defaultStatePath() string { stateHome := os.Getenv("XDG_STATE_HOME") if stateHome == "" { home, err := os.UserHomeDir() if err != nil { return filepath.Join(".", "lazy-update-manager.json") } stateHome = filepath.Join(home, ".local", "state") } return filepath.Join(stateHome, "lazy-update-manager", "state.json") } func defaultConfigPath() string { configHome := os.Getenv("XDG_CONFIG_HOME") if configHome == "" { home, err := os.UserHomeDir() if err != nil { return filepath.Join(".", "lazy-update-manager.json") } configHome = filepath.Join(home, ".config") } return filepath.Join(configHome, "lazy-update-manager", "config.json") } func tryElectronApp() error { // Try to find and launch the Electron app cmd := exec.Command("npm", "start") cmd.Stdout = nil cmd.Stderr = nil cmd.Stdin = nil if err := cmd.Run(); err != nil { // Try electron directly cmd = exec.Command("electron", ".") cmd.Stdout = nil cmd.Stderr = nil cmd.Stdin = nil return cmd.Run() } return nil } func usage() { fmt.Println(strings.TrimSpace(` LazyUpdateManager - Update helper for Arch / Hyprland Usage: lazy-update-manager status lazy-update-manager check [-quiet] lazy-update-manager notify [-force] lazy-update-manager gui [web] [-no-open] lazy-update-manager update lazy-update-manager version Commands: status Show available pacman and AUR updates check Check updates, useful for scripts and status bars notify Send a weekly desktop notification when updates exist gui Start the graphical web interface (use 'web' to force opening in the browser) update Run paru/yay -Syu when available, otherwise sudo pacman -Syu version Print the installed version and build time `)) }