commit e0f579e740ca4a06aff336ca3d25030d460763a1 Author: G34RZ Date: Sat Apr 12 12:31:31 2025 -0700 Upload files to "/" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..30d6823 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Boyd Gordon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..00fc586 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,52 @@ +# Maintainer: G34RZ +pkgbase=system76-power-gui-x11 +pkgname=system76-power-gui-x11 +pkgver=0.2.1 +pkgrel=0 +pkgdesc="GUI application for System76 Power Management" +arch=('x86_64') +url="https://gitea.dockservices.co/G34RZ/system76-power-GUI-x11.git" +license=('MIT') +depends=('system76-power' 'polkit' 'gtk3') +makedepends=('go' 'gcc' 'git') + +prepare() { + mkdir -p "$srcdir/$pkgname-$pkgver" + cd "$startdir" + cp -t "$srcdir/$pkgname-$pkgver/" \ + power-gui.go \ + system76-power-gui-x11.desktop \ + 90-system76-power-gui-x11.rules \ + LICENSE \ + go.mod \ + go.sum \ + 2>/dev/null || true +} + +build() { + cd "$srcdir/$pkgname-$pkgver" + export MAKEFLAGS="-j$(nproc)" + export CGO_CPPFLAGS="${CPPFLAGS}" + export CGO_CFLAGS="${CFLAGS}" + export CGO_CXXFLAGS="${CXXFLAGS}" + export CGO_LDFLAGS="${LDFLAGS}" + export GOFLAGS="-buildmode=pie -trimpath -mod=readonly -modcacherw" + export GOMAXPROCS=$(nproc) + go build -o system76-power-gui-x11 +} + +package() { + cd "$srcdir/$pkgname-$pkgver" + + # Install binary + install -Dm755 system76-power-gui-x11 "$pkgdir/usr/bin/system76-power-gui-x11" + + # Install X11 desktop entry + install -Dm644 system76-power-gui-x11.desktop "$pkgdir/usr/share/applications/system76-power-gui-x11.desktop" + + # Install polkit rules + install -Dm644 90-system76-power-gui-x11.rules "$pkgdir/usr/share/polkit-1/rules.d/90-system76-power-gui-x11.rules" + + # Install license + install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e43fb7 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# System76 Power GUI + +A simple graphical user interface for System76 Power Management. + +## Features +- View current power profile +- Switch between Battery, Balanced, and Turbo modes +- Automatic profile persistence +- System tray integration + +## Dependencies +- system76-power +- polkit (for privilege escalation) + +## Installation + +### Arch Linux +```bash +# Build and install the package +makepkg -si +``` + +## Usage +Launch the application from your desktop environment's application menu or run: +```bash +system76-power-gui +``` diff --git a/power-gui.go b/power-gui.go new file mode 100644 index 0000000..5d862f7 --- /dev/null +++ b/power-gui.go @@ -0,0 +1,230 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/widget" +) + +// getDisplayServerType returns the type of display server being used +func getDisplayServerType() string { + return "X11" +} + +// getDesktopEntry returns the appropriate desktop entry path +func getDesktopEntry() string { + return "/usr/share/applications/system76-power-gui-x11.desktop" +} + +// executeCommand runs a given command with sudo and returns its output or an error. +func executeCommand(command string, args ...string) (string, error) { + cmdArgs := append([]string{command}, args...) + cmd := exec.Command("pkexec", cmdArgs...) + + // Set up command environment + cmd.Env = append(os.Environ(), + "DISPLAY="+os.Getenv("DISPLAY"), + ) + + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("command execution failed: %v\nOutput: %s", err, string(output)) + } + + return string(output), nil +} + +// getStatus retrieves the current system76-power profile. +func getStatus() (string, error) { + // First check if system76-power is available + if _, err := exec.LookPath("system76-power"); err != nil { + return "", fmt.Errorf("system76-power is not installed or not in PATH: %v", err) + } + + // Try to get the profile + output, err := executeCommand("system76-power", "profile") + if err != nil { + return "", fmt.Errorf("failed to get current profile: %v", err) + } + + profile := strings.TrimSpace(output) + if profile == "" { + return "", fmt.Errorf("received empty profile from system76-power") + } + + return profile, nil +} + +// isNonCriticalError checks if the error is non-critical (like SCSI errors) +func isNonCriticalError(output string) bool { + // Check for SCSI-related errors in DBus error output + return strings.Contains(output, "failed to set scsi host profiles") && + strings.Contains(output, "org.freedesktop.DBus.Error.Failed") +} + +// trySetProfile attempts to set the profile once +func trySetProfile(profile string) error { + output, err := executeCommand("system76-power", "profile", profile) + if err != nil { + // Check if this is a non-critical error + if isNonCriticalError(output) { + // Wait a moment for the profile to be applied + time.Sleep(time.Millisecond * 500) + + // Verify if the profile was set despite the SCSI error + currentProfile, checkErr := getStatus() + if checkErr == nil && currentProfile == profile { + fmt.Printf("Profile set to %s (with non-critical SCSI warnings)\n", profile) + return nil + } + } + return fmt.Errorf("failed to set profile: %v", err) + } + return nil +} + +// setProfile sets the system76-power profile to the specified mode with one retry. +func setProfile(profile string) error { + // First attempt + err := trySetProfile(profile) + if err == nil { + fmt.Printf("Profile set to %s successfully.\n", profile) + return nil + } + + // If first attempt failed, wait a bit and retry once + fmt.Printf("First attempt failed, retrying after delay...\n") + time.Sleep(time.Second * 1) + + // Second attempt + err = trySetProfile(profile) + if err == nil { + fmt.Printf("Profile set to %s successfully on second attempt.\n", profile) + return nil + } + + return fmt.Errorf("failed to set profile to %s after retry: %v", profile, err) +} + +type powerApp struct { + app fyne.App + window fyne.Window +} + +// createTrayMenu creates the menu for the window +func (p *powerApp) createMenu() *fyne.MainMenu { + menu := fyne.NewMainMenu( + fyne.NewMenu("Power Profile", + fyne.NewMenuItem("Battery Mode", func() { + if err := setProfile("battery"); err != nil { + showError(err, p.window) + } + }), + fyne.NewMenuItem("Balanced Mode", func() { + if err := setProfile("balanced"); err != nil { + showError(err, p.window) + } + }), + fyne.NewMenuItem("Performance Mode", func() { + if err := setProfile("performance"); err != nil { + showError(err, p.window) + } + }), + fyne.NewMenuItemSeparator(), + fyne.NewMenuItem("Quit", func() { p.app.Quit() }), + ), + ) + return menu +} + +// showError displays an error in a dialog with selectable text +func showError(err error, window fyne.Window) { + errEntry := widget.NewMultiLineEntry() + errEntry.SetText(err.Error()) + errEntry.Disable() + d := dialog.NewCustom("Error", "OK", errEntry, window) + d.Resize(fyne.NewSize(400, 100)) + d.Show() +} + +// updateStatus periodically updates the status label +func (p *powerApp) updateStatus(statusLabel *widget.Label) { + for { + status, err := getStatus() + if err != nil { + fmt.Fprintf(os.Stderr, "Error updating status: %v\n", err) + time.Sleep(5 * time.Second) + continue + } + statusLabel.SetText(fmt.Sprintf("%s", status)) + time.Sleep(1 * time.Second) + } +} + +func main() { + // Create application + a := app.New() + w := a.NewWindow("System76 Power Profile") + + // Create our app structure + powerApp := &powerApp{ + app: a, + window: w, + } + + // Set up menu + w.SetMainMenu(powerApp.createMenu()) + + // Create main window content + statusLabel := widget.NewLabel("Current profile: Unknown") + + // Start background status updater + go powerApp.updateStatus(statusLabel) + + // Create profile buttons + batteryButton := widget.NewButton("Battery Mode", func() { + if err := setProfile("battery"); err != nil { + fmt.Fprintf(os.Stderr, "Error setting profile: %v\n", err) + return + } + }) + + balancedButton := widget.NewButton("Balanced Mode", func() { + if err := setProfile("balanced"); err != nil { + fmt.Fprintf(os.Stderr, "Error setting profile: %v\n", err) + return + } + }) + + performanceButton := widget.NewButton("Performance Mode", func() { + if err := setProfile("performance"); err != nil { + fmt.Fprintf(os.Stderr, "Error setting profile: %v\n", err) + return + } + }) + + // Set up window content + w.SetContent(container.NewVBox( + statusLabel, + widget.NewSeparator(), + batteryButton, + balancedButton, + performanceButton, + )) + + // Set window properties + w.Resize(fyne.NewSize(300, 200)) + w.SetCloseIntercept(func() { w.Hide() }) // Hide instead of close + + // Start application + w.Show() + a.Run() +} diff --git a/system76-power-gui-x11.desktop b/system76-power-gui-x11.desktop new file mode 100644 index 0000000..a4260a8 --- /dev/null +++ b/system76-power-gui-x11.desktop @@ -0,0 +1,22 @@ +[Desktop Entry] +Name=System76 Power GUI +Comment=GUI for System76-Power Management +Exec=system76-power-gui-x11 +Icon=preferences-system-power +Terminal=false +Type=Application +Categories=Settings;HardwareSettings; +Keywords=system76;power;profile;x11; +NoDisplay=false +Hidden=false +X-DBUS-ServiceName=org.system76.PowerGUI +X-DBUS-StartupType=none +X-GNOME-Bugzilla-Bugzilla=GNOME +X-GNOME-Bugzilla-Product=System76-Power-GUI +X-GNOME-Bugzilla-Component=General +X-GNOME-Bugzilla-Version=1.0.0 +X-GNOME-Autostart-enabled=true +Name[en_US]=System76 Power GUI X11 +Comment[en_US]=GUI for System76-Power Management +X-Ubuntu-Gettext-Domain=system76-power-gui +X-SessionType=x11