- tryElectronApp() now finds the project directory via LAZY_UPDATE_MANAGER_DIR env, CWD, binary path, or $HOME - Launches node_modules/.bin/electron directly instead of npm start (avoids recompiling the Go backend on every launch) - Add install.sh with Electron fallback, desktop launcher, and prefer_electron config - Desktop entry points to electron launcher for wofi support - Update electron to v41.7.1
372 lines
8.4 KiB
Go
372 lines
8.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 {
|
||
projectDir := electronProjectDir()
|
||
if projectDir == "" {
|
||
return errors.New("LazyUpdateManager project directory not found – set LAZY_UPDATE_MANAGER_DIR or run from the project folder")
|
||
}
|
||
|
||
electronBin := filepath.Join(projectDir, "node_modules", ".bin", "electron")
|
||
if _, err := os.Stat(electronBin); err != nil {
|
||
electronBin = "electron"
|
||
}
|
||
|
||
cmd := exec.Command(electronBin, projectDir)
|
||
cmd.Dir = projectDir
|
||
cmd.Stdout = nil
|
||
cmd.Stderr = nil
|
||
cmd.Stdin = nil
|
||
return cmd.Run()
|
||
}
|
||
|
||
func electronProjectDir() string {
|
||
if d := os.Getenv("LAZY_UPDATE_MANAGER_DIR"); d != "" {
|
||
if hasElectronMain(d) {
|
||
return d
|
||
}
|
||
}
|
||
|
||
if d, err := os.Getwd(); err == nil && hasElectronMain(d) {
|
||
return d
|
||
}
|
||
|
||
exe, err := os.Executable()
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
exeDir := filepath.Dir(exe)
|
||
|
||
// binary in ~/Projekte/LazyUpdateManager/bin/
|
||
if p := filepath.Dir(exeDir); hasElectronMain(p) {
|
||
return p
|
||
}
|
||
|
||
// binary in ~/.local/bin/, project is at configured location
|
||
if p := filepath.Join(exeDir, "..", "Projekte", "LazyUpdateManager"); hasElectronMain(p) {
|
||
return p
|
||
}
|
||
|
||
// relative to $HOME
|
||
if home, err := os.UserHomeDir(); err == nil {
|
||
if p := filepath.Join(home, "Projekte", "LazyUpdateManager"); hasElectronMain(p) {
|
||
return p
|
||
}
|
||
}
|
||
|
||
return ""
|
||
}
|
||
|
||
func hasElectronMain(dir string) bool {
|
||
info, err := os.Stat(filepath.Join(dir, "electron", "main.cjs"))
|
||
return err == nil && !info.IsDir()
|
||
}
|
||
|
||
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
|
||
`))
|
||
}
|