diff --git a/cmd/root.go b/cmd/root.go index 42c0f4a..6eddcea 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,6 +9,7 @@ import ( "path" "path/filepath" + "github.com/jedib0t/go-pretty/v6/text" "github.com/spf13/cobra" appLogging "github.com/ublue-os/uupd/pkg/logging" "golang.org/x/term" @@ -107,6 +108,15 @@ func init() { rootCmd.AddCommand(updateCheckCmd) rootCmd.AddCommand(hardwareCheckCmd) rootCmd.AddCommand(imageOutdatedCmd) + rootCmd.Flags().BoolP("hw-check", "c", false, "Run hardware check before running updates") + rootCmd.Flags().BoolP("dry-run", "n", false, "Do a dry run") + rootCmd.Flags().BoolP("verbose", "v", false, "Display command outputs after run") + rootCmd.Flags().Bool("ci", false, "Makes some modifications to behavior if is running in CI") + + rootCmd.PersistentFlags().StringVar(&fLogFile, "log-file", "-", "File where user-facing logs will be written to") + rootCmd.PersistentFlags().StringVar(&fLogLevel, "log-level", "info", "Log level for user-facing logs") + rootCmd.PersistentFlags().BoolVar(&fNoLogging, "quiet", false, "Make logs quiet") + interactiveProgress := true if fLogFile != "-" { interactiveProgress = false @@ -115,13 +125,10 @@ func init() { if !isTerminal { interactiveProgress = false } - rootCmd.Flags().BoolP("no-progress", "p", interactiveProgress, "Do not show progress bars") - rootCmd.Flags().BoolP("hw-check", "c", false, "Run hardware check before running updates") - rootCmd.Flags().BoolP("dry-run", "n", false, "Do a dry run") - rootCmd.Flags().BoolP("verbose", "v", false, "Display command outputs after run") - rootCmd.Flags().Bool("ci", false, "Makes some modifications to behavior if is running in CI") + if !text.ANSICodesSupported { + interactiveProgress = false + text.DisableColors() + } - rootCmd.PersistentFlags().StringVar(&fLogFile, "log-file", "-", "File where user-facing logs will be written to") - rootCmd.PersistentFlags().StringVar(&fLogLevel, "log-level", "info", "Log level for user-facing logs") - rootCmd.PersistentFlags().BoolVar(&fNoLogging, "quiet", false, "Make logs quiet") + rootCmd.Flags().BoolP("no-progress", "p", !interactiveProgress, "Do not show progress bars") } diff --git a/cmd/update.go b/cmd/update.go index 24c59c9..5c6c669 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -57,8 +57,8 @@ func Update(cmd *cobra.Command, args []string) { } initConfiguration := drv.UpdaterInitConfiguration{}.New() - _, empty := os.LookupEnv("CI") - initConfiguration.Ci = !empty + _, exists := os.LookupEnv("CI") + initConfiguration.Ci = exists initConfiguration.DryRun = dryRun initConfiguration.Verbose = verboseRun @@ -130,6 +130,7 @@ func Update(cmd *cobra.Command, args []string) { if progressEnabled { go pw.Render() + lib.ResetOscProgress() } // -1 because 0 index @@ -188,12 +189,15 @@ func Update(cmd *cobra.Command, args []string) { tracker.IncrementSection(err) } - pw.Stop() + if progressEnabled { + pw.Stop() + lib.ResetOscProgress() + } if verboseRun { slog.Info("Verbose run requested") for _, output := range outputs { - slog.Info("CommandOutput", slog.String("context", output.Context), slog.String("stdout", output.Stdout), slog.Any("stderr", output.Stderr)) + slog.Info(output.Context, slog.String("stdout", output.Stdout), slog.Any("stderr", output.Stderr), slog.Any("cli", output.Cli)) } return @@ -210,7 +214,7 @@ func Update(cmd *cobra.Command, args []string) { slog.Warn("Exited with failed updates.") for _, output := range failures { - slog.Info("CommandOutput", slog.String("context", output.Context), slog.String("stdout", output.Stdout), slog.Any("stderr", output.Stderr)) + slog.Info(output.Context, slog.String("stdout", output.Stdout), slog.Any("stderr", output.Stderr), slog.Any("cli", output.Cli)) } return diff --git a/drv/brew.go b/drv/brew.go index 155862b..918c630 100644 --- a/drv/brew.go +++ b/drv/brew.go @@ -8,17 +8,17 @@ import ( ) func (up BrewUpdater) GetBrewUID() (int, error) { - inf, err := os.Stat(up.Environment["HOMEBREW_PREFIX"]) + inf, err := os.Stat(up.BrewPrefix) if err != nil { return -1, err } if !inf.IsDir() { - return -1, fmt.Errorf("Brew prefix: %v, is not a dir.", up.Environment["HOMEBREW_PREFIX"]) + return -1, fmt.Errorf("Brew prefix: %v, is not a dir.", up.BrewPrefix) } stat, ok := inf.Sys().(*syscall.Stat_t) if !ok { - return -1, fmt.Errorf("Unable to retriev UID info for %v", up.Environment["HOMEBREW_PREFIX"]) + return -1, fmt.Errorf("Unable to retriev UID info for %v", up.BrewPrefix) } return int(stat.Uid), nil } @@ -42,59 +42,71 @@ func (up BrewUpdater) Update() (*[]CommandOutput, error) { return &final_output, nil } - out, err := lib.RunUID(up.BaseUser, []string{up.Environment["HOMEBREW_PATH"], "update"}, up.Environment) + cli := []string{up.BrewPath, "update"} + out, err := lib.RunUID(up.BaseUser, cli, up.Config.Environment) tmpout := CommandOutput{}.New(out, err) + tmpout.Context = "Brew Update" + tmpout.Cli = cli + tmpout.Failure = err != nil if err != nil { tmpout.SetFailureContext("Brew update") final_output = append(final_output, *tmpout) return &final_output, err } - out, err = lib.RunUID(up.BaseUser, []string{up.Environment["HOMEBREW_PATH"], "upgrade"}, up.Environment) + cli = []string{up.BrewPath, "upgrade"} + out, err = lib.RunUID(up.BaseUser, cli, up.Config.Environment) tmpout = CommandOutput{}.New(out, err) - if err != nil { - tmpout.SetFailureContext("Brew upgrade") - } + tmpout.Context = "Brew Upgrade" + tmpout.Cli = cli + tmpout.Failure = err != nil final_output = append(final_output, *tmpout) return &final_output, err } type BrewUpdater struct { - Config DriverConfiguration - BaseUser int - Environment map[string]string + Config DriverConfiguration + BaseUser int + BrewRepo string + BrewPrefix string + BrewCellar string + BrewPath string } func (up BrewUpdater) New(config UpdaterInitConfiguration) (BrewUpdater, error) { - brewPrefix, empty := os.LookupEnv("HOMEBREW_PREFIX") - if empty || brewPrefix == "" { - brewPrefix = "/home/linuxbrew/.linuxbrew" - } - brewRepo, empty := os.LookupEnv("HOMEBREW_REPOSITORY") - if empty || brewRepo == "" { - brewRepo = fmt.Sprintf("%s/Homebrew", brewPrefix) - } - brewCellar, empty := os.LookupEnv("HOMEBREW_CELLAR") - if empty || brewCellar == "" { - brewCellar = fmt.Sprintf("%s/Cellar", brewPrefix) - } - brewPath, empty := os.LookupEnv("HOMEBREW_PATH") - if empty || brewPath == "" { - brewPath = fmt.Sprintf("%s/bin/brew", brewPrefix) - } - up.Environment = map[string]string{ - "HOMEBREW_PREFIX": brewPrefix, - "HOMEBREW_REPOSITORY": brewRepo, - "HOMEBREW_CELLAR": brewCellar, - "HOMEBREW_PATH": brewPath, - } up.Config = DriverConfiguration{ Title: "Brew", Description: "CLI Apps", Enabled: true, MultiUser: false, DryRun: config.DryRun, + Environment: config.Environment, + } + + brewPrefix, exists := up.Config.Environment["HOMEBREW_PREFIX"] + if !exists || brewPrefix == "" { + up.BrewPrefix = "/home/linuxbrew/.linuxbrew" + } else { + up.BrewPrefix = brewPrefix + } + brewRepo, exists := up.Config.Environment["HOMEBREW_REPOSITORY"] + if !exists || brewRepo == "" { + up.BrewRepo = fmt.Sprintf("%s/Homebrew", up.BrewPrefix) + } else { + up.BrewRepo = brewRepo + } + brewCellar, exists := up.Config.Environment["HOMEBREW_CELLAR"] + if !exists || brewCellar == "" { + up.BrewCellar = fmt.Sprintf("%s/Cellar", up.BrewPrefix) + } else { + up.BrewCellar = brewCellar + } + brewPath, exists := up.Config.Environment["HOMEBREW_PATH"] + if !exists || brewPath == "" { + up.BrewPath = fmt.Sprintf("%s/bin/brew", up.BrewPrefix) + } else { + up.BrewPath = brewPath } if up.Config.DryRun { @@ -105,7 +117,7 @@ func (up BrewUpdater) New(config UpdaterInitConfiguration) (BrewUpdater, error) if err != nil { return up, err } - up.BaseUser = uid + return up, nil } diff --git a/drv/distrobox.go b/drv/distrobox.go index 958dfa1..115d893 100644 --- a/drv/distrobox.go +++ b/drv/distrobox.go @@ -1,14 +1,13 @@ package drv import ( - "fmt" - "github.com/ublue-os/uupd/lib" ) type DistroboxUpdater struct { Config DriverConfiguration Tracker *TrackerConfiguration + binaryPath string users []lib.User usersEnabled bool } @@ -24,7 +23,7 @@ func (up DistroboxUpdater) Steps() int { return 0 } -func (up DistroboxUpdater) New(initconfig UpdaterInitConfiguration) (DistroboxUpdater, error) { +func (up DistroboxUpdater) New(config UpdaterInitConfiguration) (DistroboxUpdater, error) { userdesc := "Distroboxes for User:" up.Config = DriverConfiguration{ Title: "Distrobox", @@ -32,11 +31,19 @@ func (up DistroboxUpdater) New(initconfig UpdaterInitConfiguration) (DistroboxUp UserDescription: &userdesc, Enabled: true, MultiUser: true, - DryRun: initconfig.DryRun, + DryRun: config.DryRun, + Environment: config.Environment, } up.usersEnabled = false up.Tracker = nil + binaryPath, exists := up.Config.Environment["UUPD_DISTROBOX_BINARY"] + if !exists || binaryPath == "" { + up.binaryPath = "/usr/bin/distrobox" + } else { + up.binaryPath = binaryPath + } + return up, nil } @@ -64,24 +71,26 @@ func (up *DistroboxUpdater) Update() (*[]CommandOutput, error) { return &finalOutput, nil } - // TODO: add env support for Flatpak and Distrobox updaters lib.ChangeTrackerMessageFancy(*up.Tracker.Writer, up.Tracker.Tracker, up.Tracker.Progress, lib.TrackerMessage{Title: up.Config.Title, Description: up.Config.Description}) - out, err := lib.RunUID(0, []string{"/usr/bin/distrobox", "upgrade", "-a"}, nil) + cli := []string{up.binaryPath, "upgrade", "-a"} + out, err := lib.RunUID(0, cli, nil) tmpout := CommandOutput{}.New(out, err) - if err != nil { - tmpout.SetFailureContext("System Distroboxes") - } + tmpout.Context = up.Config.Description + tmpout.Cli = cli + tmpout.Failure = err != nil finalOutput = append(finalOutput, *tmpout) err = nil for _, user := range up.users { up.Tracker.Tracker.IncrementSection(err) + context := *up.Config.UserDescription + " " + user.Name lib.ChangeTrackerMessageFancy(*up.Tracker.Writer, up.Tracker.Tracker, up.Tracker.Progress, lib.TrackerMessage{Title: up.Config.Title, Description: *up.Config.UserDescription + " " + user.Name}) - out, err := lib.RunUID(user.UID, []string{"/usr/bin/distrobox", "upgrade", "-a"}, nil) + cli := []string{up.binaryPath, "upgrade", "-a"} + out, err := lib.RunUID(user.UID, cli, nil) tmpout = CommandOutput{}.New(out, err) - if err != nil { - tmpout.SetFailureContext(fmt.Sprintf("Distroboxes for User: %s", user.Name)) - } + tmpout.Context = context + tmpout.Cli = cli + tmpout.Failure = err != nil finalOutput = append(finalOutput, *tmpout) } return &finalOutput, nil diff --git a/drv/flatpak.go b/drv/flatpak.go index e2a7137..7b524c6 100644 --- a/drv/flatpak.go +++ b/drv/flatpak.go @@ -1,7 +1,6 @@ package drv import ( - "fmt" "os/exec" "github.com/ublue-os/uupd/lib" @@ -10,6 +9,7 @@ import ( type FlatpakUpdater struct { Config DriverConfiguration Tracker *TrackerConfiguration + binaryPath string users []lib.User usersEnabled bool } @@ -25,7 +25,7 @@ func (up FlatpakUpdater) Steps() int { return 0 } -func (up FlatpakUpdater) New(initconfig UpdaterInitConfiguration) (FlatpakUpdater, error) { +func (up FlatpakUpdater) New(config UpdaterInitConfiguration) (FlatpakUpdater, error) { userdesc := "Apps for User:" up.Config = DriverConfiguration{ Title: "Flatpak", @@ -33,11 +33,19 @@ func (up FlatpakUpdater) New(initconfig UpdaterInitConfiguration) (FlatpakUpdate UserDescription: &userdesc, Enabled: true, MultiUser: true, - DryRun: initconfig.DryRun, + DryRun: config.DryRun, + Environment: config.Environment, } up.usersEnabled = false up.Tracker = nil + binaryPath, exists := up.Config.Environment["UUPD_FLATPAK_BINARY"] + if !exists || binaryPath == "" { + up.binaryPath = "/usr/bin/flatpak" + } else { + up.binaryPath = binaryPath + } + return up, nil } @@ -66,23 +74,26 @@ func (up FlatpakUpdater) Update() (*[]CommandOutput, error) { } lib.ChangeTrackerMessageFancy(*up.Tracker.Writer, up.Tracker.Tracker, up.Tracker.Progress, lib.TrackerMessage{Title: up.Config.Title, Description: up.Config.Description}) - flatpakCmd := exec.Command("/usr/bin/flatpak", "update", "-y") + cli := []string{up.binaryPath, "update", "-y"} + flatpakCmd := exec.Command(cli[0], cli[1:]...) out, err := flatpakCmd.CombinedOutput() tmpout := CommandOutput{}.New(out, err) - if err != nil { - tmpout.SetFailureContext("Flatpak System Apps") - } + tmpout.Context = up.Config.Description + tmpout.Cli = cli + tmpout.Failure = err != nil finalOutput = append(finalOutput, *tmpout) err = nil for _, user := range up.users { up.Tracker.Tracker.IncrementSection(err) - lib.ChangeTrackerMessageFancy(*up.Tracker.Writer, up.Tracker.Tracker, up.Tracker.Progress, lib.TrackerMessage{Title: up.Config.Title, Description: *up.Config.UserDescription + " " + user.Name}) - out, err := lib.RunUID(user.UID, []string{"/usr/bin/flatpak", "update", "-y"}, nil) + context := *up.Config.UserDescription + " " + user.Name + lib.ChangeTrackerMessageFancy(*up.Tracker.Writer, up.Tracker.Tracker, up.Tracker.Progress, lib.TrackerMessage{Title: up.Config.Title, Description: context}) + cli := []string{up.binaryPath, "update", "-y"} + out, err := lib.RunUID(user.UID, cli, nil) tmpout = CommandOutput{}.New(out, err) - if err != nil { - tmpout.SetFailureContext(fmt.Sprintf("Flatpak User: %s", user.Name)) - } + tmpout.Context = context + tmpout.Cli = cli + tmpout.Failure = err != nil finalOutput = append(finalOutput, *tmpout) } return &finalOutput, nil diff --git a/drv/generic.go b/drv/generic.go index c708ad8..133fe74 100644 --- a/drv/generic.go +++ b/drv/generic.go @@ -8,11 +8,13 @@ import ( "github.com/ublue-os/uupd/lib" ) +type EnvironmentMap map[string]string + type UpdaterInitConfiguration struct { DryRun bool Ci bool Verbose bool - Environment map[string]string + Environment EnvironmentMap } func GetEnvironment(data []string, getkeyval func(item string) (key, val string)) map[string]string { @@ -42,6 +44,7 @@ type CommandOutput struct { Failure bool Stderr error Context string + Cli []string } func (output CommandOutput) New(out []byte, err error) *CommandOutput { @@ -64,6 +67,7 @@ type DriverConfiguration struct { Enabled bool MultiUser bool DryRun bool + Environment EnvironmentMap UserDescription *string } diff --git a/drv/rpm-ostree.go b/drv/rpm-ostree.go index 8eae935..dca9bf4 100644 --- a/drv/rpm-ostree.go +++ b/drv/rpm-ostree.go @@ -96,13 +96,14 @@ func (up RpmOstreeUpdater) New(config UpdaterInitConfiguration) (RpmOstreeUpdate Description: "System Updates", Enabled: !config.Ci, DryRun: config.DryRun, + Environment: config.Environment, } if up.Config.DryRun { return up, nil } - binaryPath, exists := config.Environment["UUPD_RPMOSTREE_BINARY"] + binaryPath, exists := up.Config.Environment["UUPD_RPMOSTREE_BINARY"] if !exists || binaryPath == "" { up.BinaryPath = "/usr/bin/rpm-ostree" } else { diff --git a/drv/system.go b/drv/system.go index 21d4fec..f4c5784 100644 --- a/drv/system.go +++ b/drv/system.go @@ -99,13 +99,14 @@ func (up SystemUpdater) New(config UpdaterInitConfiguration) (SystemUpdater, err Description: "System Updates", Enabled: !config.Ci, DryRun: config.DryRun, + Environment: config.Environment, } if up.Config.DryRun { return up, nil } - bootcBinaryPath, exists := config.Environment["UUPD_BOOTC_BINARY"] + bootcBinaryPath, exists := up.Config.Environment["UUPD_BOOTC_BINARY"] if !exists || bootcBinaryPath == "" { up.BinaryPath = "/usr/bin/bootc" } else { diff --git a/lib/colorpicker.go b/lib/colorpicker.go new file mode 100644 index 0000000..8993686 --- /dev/null +++ b/lib/colorpicker.go @@ -0,0 +1,70 @@ +package lib + +import ( + "math" + + . "github.com/jedib0t/go-pretty/v6/text" +) + +// Accent color portal return as of xdg-desktop-portal-gnome 47.1 +type Accent struct { + Type string `json:"type"` + Data []struct { + Type string `json:"type"` + Data [3]float64 `json:"data"` + } `json:"data"` +} + +// Colors taken straight from GNOME 47 accent colors using this command: +// busctl --user call org.freedesktop.portal.Desktop /org/freedesktop/portal/desktop org.freedesktop.portal.Settings ReadOne 'ss' 'org.freedesktop.appearance' 'accent-color' +// This is as close as we can map the colors as possible afaik - Pink and Magenta DO look a like, and thats kind of a problem +var colorMap = map[Color][3]float64{ + FgHiBlack: {0, 0, 0}, + FgHiBlue: {0.207843, 0.517647, 0.894118}, + FgHiCyan: {0.129412, 0.564706, 0.643137}, + FgHiGreen: {0.227451, 0.580392, 0.290196}, + FgHiYellow: {0.784314, 0.533333, 0}, + FgHiRed: {0.901961, 0.176471, 0.258824}, + FgHiMagenta: {0.568627, 0.254902, 0.67451}, + FgHiWhite: {0.435294, 0.513726, 0.588235}, +} + +// Calculates the Euclidean distance between two colors +func colorDistance(c1, c2 [3]float64) float64 { + return math.Sqrt( + math.Pow(c1[0]-c2[0], 2) + + math.Pow(c1[1]-c2[1], 2) + + math.Pow(c1[2]-c2[2], 2), + ) +} + +func findClosestColor(rgb [3]float64) (Color, Color) { + var closestColor Color + minDistance := math.MaxFloat64 + + for color, predefinedRGB := range colorMap { + distance := colorDistance(rgb, predefinedRGB) + if distance < minDistance { + minDistance = distance + closestColor = color + } + } + + nonHiColor, isHiColor := hiToNonHiMap[closestColor] + if isHiColor { + return closestColor, nonHiColor + } + + return closestColor, closestColor +} + +var hiToNonHiMap = map[Color]Color{ + FgHiBlack: FgBlack, + FgHiRed: FgRed, + FgHiGreen: FgGreen, + FgHiYellow: FgYellow, + FgHiBlue: FgBlue, + FgHiMagenta: FgMagenta, + FgHiCyan: FgCyan, + FgHiWhite: FgWhite, +} diff --git a/lib/percentmanager.go b/lib/percentmanager.go index 24ca758..1aab529 100644 --- a/lib/percentmanager.go +++ b/lib/percentmanager.go @@ -1,8 +1,12 @@ package lib import ( + "encoding/json" "fmt" "log/slog" + "math" + "os" + "strconv" "time" "github.com/jedib0t/go-pretty/v6/progress" @@ -33,6 +37,7 @@ var CuteColors = progress.StyleColors{ func NewProgressWriter() progress.Writer { pw := progress.NewWriter() + pw.SetTrackerLength(25) pw.Style().Visibility.TrackerOverall = true pw.Style().Visibility.Time = true @@ -44,7 +49,47 @@ func NewProgressWriter() progress.Writer { pw.SetTrackerPosition(progress.PositionRight) pw.SetUpdateFrequency(time.Millisecond * 100) pw.Style().Options.PercentFormat = "%4.1f%%" - pw.Style().Colors = CuteColors + + colorsSet := CuteColors + pw.Style().Colors = colorsSet + + var targetUser int + baseUser, exists := os.LookupEnv("SUDO_UID") + if !exists || baseUser == "" { + targetUser = 0 + } else { + var err error + targetUser, err = strconv.Atoi(baseUser) + if err != nil { + slog.Error("Failed parsing provided user as UID", slog.String("user_value", baseUser)) + return pw + } + } + + if targetUser != 0 { + cli := []string{"busctl", "--user", "--json=short", "call", "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.Settings", "ReadOne", "ss", "org.freedesktop.appearance", "accent-color"} + out, err := RunUID(targetUser, cli, nil) + var accent Accent + err = json.Unmarshal(out, &accent) + if err != nil { + pw.Style().Colors = colorsSet + return pw + } + + raw_color := accent.Data[0].Data + + highlightColor, lowColor := findClosestColor(raw_color) + + validHighlightColor := text.Colors{highlightColor} + validLowColor := text.Colors{lowColor} + + colorsSet.Percent = validHighlightColor + colorsSet.Tracker = validHighlightColor + colorsSet.Time = validLowColor + colorsSet.Value = validLowColor + colorsSet.Speed = validLowColor + } + pw.Style().Colors = colorsSet return pw } @@ -70,6 +115,8 @@ func ChangeTrackerMessageFancy(writer progress.Writer, tracker *IncrementTracker ) return } + percentage := math.Round((float64(tracker.Tracker.Value()) / float64(tracker.Tracker.Total)) * 100) + fmt.Printf("\033]9;4;1;%d\a", int(percentage)) finalMessage := fmt.Sprintf("Updating %s (%s)", message.Description, message.Title) writer.SetMessageLength(len(finalMessage)) tracker.Tracker.UpdateMessage(finalMessage) @@ -93,3 +140,9 @@ func (it *IncrementTracker) IncrementSection(err error) { func (it *IncrementTracker) CurrentStep() int { return it.incrementer.doneIncrements } + +func ResetOscProgress() { + // OSC escape sequence to reset all previous OSC progress hints to 0%. + // Documentation is on https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC + print("\033]9;4;0\a") +}