Upload files to "/"

This commit is contained in:
G34RZ 2025-04-12 12:31:31 -07:00
commit e0f579e740
5 changed files with 352 additions and 0 deletions

21
LICENSE Normal file
View File

@ -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.

52
PKGBUILD Normal file
View File

@ -0,0 +1,52 @@
# Maintainer: G34RZ <g3arz@dockservices.co>
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"
}

27
README.md Normal file
View File

@ -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
```

230
power-gui.go Normal file
View File

@ -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()
}

View File

@ -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