Files
LazyUpdateManager/cmd/lazy-update-manager/main.go

328 lines
7.4 KiB
Go

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
`))
}