diff --git a/desktop/icons/ham.png b/desktop/icons/ham.png deleted file mode 100644 index 59c8999..0000000 Binary files a/desktop/icons/ham.png and /dev/null differ diff --git a/hampack-src/go.mod b/hampack-src/go.mod deleted file mode 100644 index 37e28f5..0000000 --- a/hampack-src/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module hampack - -go 1.22 - -require ( - github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20250703085708-8e7f01e2c815 - github.com/diamondburned/gotk4/pkg v0.3.2-0.20250703063411-16654385f59a -) diff --git a/hampack-src/main.go b/hampack-src/main.go deleted file mode 100644 index a4b89be..0000000 --- a/hampack-src/main.go +++ /dev/null @@ -1,637 +0,0 @@ -package main - -import ( - "bufio" - "encoding/json" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "regexp" - "sort" - "strings" - - "github.com/diamondburned/gotk4-adwaita/pkg/adw" - "github.com/diamondburned/gotk4/pkg/gdk/v4" - "github.com/diamondburned/gotk4/pkg/glib/v2" - "github.com/diamondburned/gotk4/pkg/gtk/v4" -) - -// ── Helpers ──────────────────────────────────────────────────────────────────── - -func homeDir() string { - d, _ := os.UserHomeDir() - return d -} - -func hampackDir() string { - if d := os.Getenv("HAMPACK_DIR"); d != "" { - return d - } - return filepath.Join(homeDir(), ".local/share/HamPack") -} - -func stateFilePath() string { - return filepath.Join(homeDir(), ".local/state/HamPack/selections.json") -} - -func expand(v string) string { - return strings.ReplaceAll(v, "$HOME", homeDir()) -} - -var windowsItems = [][2]string{ - {"VARA HF", filepath.Join(homeDir(), ".wine/drive_c/VARA")}, - {"VARA FM", filepath.Join(homeDir(), ".wine/drive_c/VARA FM")}, - {"VARA Terminal", filepath.Join(homeDir(), ".wine/drive_c/VARA Terminal")}, - {"Winlink Express", filepath.Join(homeDir(), ".wine/drive_c/RMS Express")}, -} - -// ── Config parsing ───────────────────────────────────────────────────────────── - -func parseINI(path string) map[string]map[string]string { - entries := make(map[string]map[string]string) - data, err := os.ReadFile(path) - if err != nil { - return entries - } - var cur string - for _, line := range strings.Split(string(data), "\n") { - line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - if len(line) > 2 && line[0] == '[' && line[len(line)-1] == ']' { - cur = line[1 : len(line)-1] - entries[cur] = make(map[string]string) - continue - } - if cur == "" { - continue - } - if k, v, ok := strings.Cut(line, "="); ok { - entries[cur][strings.TrimSpace(k)] = strings.TrimSpace(v) - } - } - return entries -} - -func parsePackages() (utils, apps []string) { - path := filepath.Join(hampackDir(), "packages.conf") - cmd := exec.Command("bash", "-c", fmt.Sprintf( - `source '%s' && printf '%%s\n' ${UTILITIES[@]} && echo '---' && printf '%%s\n' ${APPLICATIONS[@]}`, - path)) - out, err := cmd.Output() - if err != nil { - return - } - inApps := false - for _, line := range strings.Split(string(out), "\n") { - line = strings.TrimSpace(line) - if line == "---" { - inApps = true - } else if line != "" { - if inApps { - apps = append(apps, line) - } else { - utils = append(utils, line) - } - } - } - return -} - -func parseFlatpaks() []string { - path := filepath.Join(hampackDir(), "install-flatpaks.sh") - data, err := os.ReadFile(path) - if err != nil { - return nil - } - re := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]*(\.[a-zA-Z][a-zA-Z0-9_-]*){2,}$`) - var ids []string - for _, line := range strings.Split(string(data), "\n") { - s := strings.Trim(strings.TrimSpace(line), `"'`) - if re.MatchString(s) { - ids = append(ids, s) - } - } - return ids -} - -// ── State ────────────────────────────────────────────────────────────────────── - -// installedPacmanPackages returns a set of all installed pacman package names. -func installedPacmanPackages() map[string]bool { - out, err := exec.Command("pacman", "-Q").Output() - if err != nil { - return map[string]bool{} - } - installed := make(map[string]bool) - for _, line := range strings.Split(string(out), "\n") { - if fields := strings.Fields(line); len(fields) > 0 { - installed[fields[0]] = true - } - } - return installed -} - -// installedFlatpaks returns a set of all installed Flatpak application IDs. -func installedFlatpaks() map[string]bool { - out, err := exec.Command("flatpak", "list", "--app", "--columns=application").Output() - if err != nil { - return map[string]bool{} - } - installed := make(map[string]bool) - for _, line := range strings.Split(string(out), "\n") { - if s := strings.TrimSpace(line); s != "" { - installed[s] = true - } - } - return installed -} - -// detectInitialState checks actual installation status for every item. -func detectInitialState( - utils, apps, flatpaks []string, - compiled, binaries map[string]map[string]string, -) map[string]bool { - pacman := installedPacmanPackages() - flatpak := installedFlatpaks() - state := make(map[string]bool) - - for _, name := range utils { - state["utility:"+name] = pacman[name] - } - for _, name := range apps { - state["application:"+name] = pacman[name] - } - for _, id := range flatpaks { - state["flatpak:"+id] = flatpak[id] - } - for name, fields := range compiled { - path := expand(fields["install"]) - _, err := os.Stat(path) - state["compiled:"+name] = err == nil - } - for name, fields := range binaries { - path := expand(fields["install"]) - _, err := os.Stat(path) - state["binary:"+name] = err == nil - } - for _, item := range windowsItems { - _, err := os.Stat(item[1]) - state["windows:"+item[0]] = err == nil - } - - return state -} - -func saveState(state map[string]bool) { - os.MkdirAll(filepath.Dir(stateFilePath()), 0755) - data, _ := json.MarshalIndent(state, "", " ") - os.WriteFile(stateFilePath(), data, 0644) -} - - -// ── Operations ──────────────────────────────────────────────────────────────── - -type Op struct { - Label string - Cmds [][]string - Env []string // nil means inherit os.Environ() - Tmp string // temp file to clean up after the op -} - -func writeTempINI(name string, fields map[string]string) string { - f, err := os.CreateTemp("", "hampack-*.conf") - if err != nil { - return "" - } - defer f.Close() - fmt.Fprintf(f, "[%s]\n", name) - for k, v := range fields { - fmt.Fprintf(f, "%s=%s\n", k, v) - } - return f.Name() -} - -func sortedKeys(m map[string]map[string]string) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -func computeOps( - oldState, newState map[string]bool, - compiled, binaries map[string]map[string]string, -) []Op { - allKeys := make(map[string]bool) - for k := range oldState { - allKeys[k] = true - } - for k := range newState { - allKeys[k] = true - } - keys := make([]string, 0, len(allKeys)) - for k := range allKeys { - keys = append(keys, k) - } - sort.Strings(keys) - - var ops []Op - windowsInstallAdded := false - - for _, key := range keys { - was := oldState[key] - now := newState[key] - if was == now { - continue - } - parts := strings.SplitN(key, ":", 2) - if len(parts) != 2 { - continue - } - cat, name := parts[0], parts[1] - - if now { // ── install ────────────────────────────────────────────────── - switch cat { - case "utility", "application": - ops = append(ops, Op{ - Label: "Install " + name, - Cmds: [][]string{{"yay", "-S", "--noconfirm", name}}, - }) - case "flatpak": - ops = append(ops, Op{ - Label: "Install " + name, - Cmds: [][]string{{"flatpak", "install", "--noninteractive", "flathub", name}}, - }) - case "compiled": - if fields, ok := compiled[name]; ok { - tmp := writeTempINI(name, fields) - ops = append(ops, Op{ - Label: "Build " + name, - Cmds: [][]string{{"bash", filepath.Join(hampackDir(), "install-compiled.sh")}}, - Env: append(os.Environ(), "HAMPACK_COMPILE_CONF="+tmp), - Tmp: tmp, - }) - } - case "binary": - if fields, ok := binaries[name]; ok { - tmp := writeTempINI(name, fields) - ops = append(ops, Op{ - Label: "Install " + name, - Cmds: [][]string{{"bash", filepath.Join(hampackDir(), "install-binaries.sh")}}, - Env: append(os.Environ(), "HAMPACK_BINARIES_CONF="+tmp), - Tmp: tmp, - }) - } - case "windows": - if !windowsInstallAdded { - ops = append(ops, Op{ - Label: "Install Windows Apps (Wine)", - Cmds: [][]string{{"bash", filepath.Join(hampackDir(), "install-windows-apps.sh")}}, - }) - windowsInstallAdded = true - } - } - } else { // ── uninstall ──────────────────────────────────────────────── - switch cat { - case "utility", "application": - ops = append(ops, Op{ - Label: "Remove " + name, - Cmds: [][]string{{"sudo", "pacman", "-Rns", "--noconfirm", name}}, - }) - case "flatpak": - ops = append(ops, Op{ - Label: "Remove " + name, - Cmds: [][]string{{"flatpak", "uninstall", "--noninteractive", name}}, - }) - case "compiled": - if fields, ok := compiled[name]; ok { - installPath := expand(fields["install"]) - desktopSrc := expand(fields["desktop"]) - cmds := [][]string{{"rm", "-f", installPath}} - if desktopSrc != "" { - cmds = append(cmds, []string{"rm", "-f", desktopSrc}) - } - cmds = append(cmds, []string{"rm", "-f", - filepath.Join(homeDir(), ".local/share/applications", name+".desktop")}) - ops = append(ops, Op{Label: "Remove " + name, Cmds: cmds}) - } - case "binary": - if fields, ok := binaries[name]; ok { - installPath := expand(fields["install"]) - ops = append(ops, Op{ - Label: "Remove " + name, - Cmds: [][]string{ - {"rm", "-f", installPath}, - {"rm", "-f", filepath.Join(homeDir(), ".local/share/applications", name+".desktop")}, - }, - }) - } - case "windows": - for _, item := range windowsItems { - if item[0] == name { - ops = append(ops, Op{ - Label: "Remove " + name, - Cmds: [][]string{{"rm", "-rf", item[1]}}, - }) - break - } - } - } - } - } - return ops -} - -// ── Output window ───────────────────────────────────────────────────────────── - -type outputWin struct { - *gtk.Window - buf *gtk.TextBuffer - tv *gtk.TextView - endMark *gtk.TextMark - closeBtn *gtk.Button -} - -func newOutputWin(ops []Op, onDone func()) *outputWin { - w := &outputWin{Window: gtk.NewWindow()} - w.SetTitle("Applying Changes") - w.SetDefaultSize(680, 500) - - toolbar := adw.NewToolbarView() - w.SetChild(toolbar) - - header := adw.NewHeaderBar() - header.SetShowEndTitleButtons(false) - toolbar.AddTopBar(header) - - w.closeBtn = gtk.NewButtonWithLabel("Close") - w.closeBtn.SetSensitive(false) - w.closeBtn.ConnectClicked(func() { w.Close() }) - header.PackEnd(w.closeBtn) - - scroll := gtk.NewScrolledWindow() - scroll.SetPolicy(gtk.PolicyAutomatic, gtk.PolicyAlways) - scroll.SetVExpand(true) - scroll.SetHExpand(true) - - w.buf = gtk.NewTextBuffer(nil) - endIter := w.buf.EndIter() - w.endMark = w.buf.CreateMark("end", endIter, false) - - w.tv = gtk.NewTextView() - w.tv.SetBuffer(w.buf) - w.tv.SetEditable(false) - w.tv.SetCursorVisible(false) - w.tv.SetMonospace(true) - w.tv.SetLeftMargin(12) - w.tv.SetRightMargin(12) - w.tv.SetTopMargin(8) - w.tv.SetBottomMargin(8) - w.tv.SetWrapMode(gtk.WrapWordChar) - scroll.SetChild(w.tv) - toolbar.SetContent(scroll) - - go w.run(ops, onDone) - return w -} - -func (w *outputWin) append(text string) { - glib.IdleAdd(func() bool { - iter := w.buf.EndIter() - w.buf.Insert(iter, text) - w.tv.ScrollToMark(w.endMark, 0, false, 0, 1) - return false - }) -} - -func (w *outputWin) runCmd(cmd, env []string) bool { - w.append(fmt.Sprintf("$ %s\n", strings.Join(cmd, " "))) - - c := exec.Command(cmd[0], cmd[1:]...) - c.Dir = hampackDir() - if env != nil { - c.Env = env - } - - pr, pw := io.Pipe() - c.Stdout = pw - c.Stderr = pw - - scanDone := make(chan struct{}) - go func() { - defer close(scanDone) - scanner := bufio.NewScanner(pr) - for scanner.Scan() { - w.append(scanner.Text() + "\n") - } - }() - - if err := c.Start(); err != nil { - pw.Close() - <-scanDone - pr.Close() - w.append(fmt.Sprintf("Error: %v\n", err)) - return false - } - - err := c.Wait() - pw.Close() - <-scanDone - pr.Close() - - if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - w.append(fmt.Sprintf("[exit %d]\n", exitErr.ExitCode())) - } - return false - } - return true -} - -func (w *outputWin) run(ops []Op, onDone func()) { - success := true - for _, op := range ops { - w.append(fmt.Sprintf("\n── %s ──\n", op.Label)) - for _, cmd := range op.Cmds { - if !w.runCmd(cmd, op.Env) { - success = false - } - } - if op.Tmp != "" { - os.Remove(op.Tmp) - } - } - if success { - w.append("\n── Done ──\n") - } else { - w.append("\n── Completed with errors ──\n") - } - glib.IdleAdd(func() bool { - w.closeBtn.SetSensitive(true) - onDone() - return false - }) -} - -// ── Main window ─────────────────────────────────────────────────────────────── - -type mainWin struct { - *adw.ApplicationWindow - compiled map[string]map[string]string - binaries map[string]map[string]string - state map[string]bool - rows map[string]*gtk.Switch - applyBtn *gtk.Button - updateBtn *gtk.Button -} - -func newMainWin(app *adw.Application) *mainWin { - compiled := parseINI(filepath.Join(hampackDir(), "compile.conf")) - binaries := parseINI(filepath.Join(hampackDir(), "binaries.conf")) - utils, apps := parsePackages() - flatpaks := parseFlatpaks() - - w := &mainWin{ - ApplicationWindow: adw.NewApplicationWindow(&app.Application), - compiled: compiled, - binaries: binaries, - state: detectInitialState(utils, apps, flatpaks, compiled, binaries), - rows: make(map[string]*gtk.Switch), - } - w.SetTitle("HamPack Manager") - w.SetDefaultSize(600, 720) - - toolbar := adw.NewToolbarView() - w.SetContent(toolbar) - - header := adw.NewHeaderBar() - toolbar.AddTopBar(header) - - w.applyBtn = gtk.NewButtonWithLabel("Apply Changes") - w.applyBtn.AddCSSClass("suggested-action") - w.applyBtn.ConnectClicked(func() { w.onApply() }) - header.PackEnd(w.applyBtn) - - w.updateBtn = gtk.NewButtonWithLabel("Run Update") - w.updateBtn.ConnectClicked(func() { w.onUpdate() }) - header.PackStart(w.updateBtn) - - page := adw.NewPreferencesPage() - toolbar.SetContent(page) - - windows := make([]string, len(windowsItems)) - for i, item := range windowsItems { - windows[i] = item[0] - } - - w.makeGroup(page, "System Utilities", "utility", utils) - w.makeGroup(page, "AUR Applications", "application", apps) - w.makeGroup(page, "Flatpaks", "flatpak", flatpaks) - w.makeGroup(page, "Compiled from Source", "compiled", sortedKeys(w.compiled)) - w.makeGroup(page, "Pre-built Binaries", "binary", sortedKeys(w.binaries)) - w.makeGroup(page, "Windows Apps (Wine)", "windows", windows) - - return w -} - -func (w *mainWin) makeGroup(page *adw.PreferencesPage, title, cat string, names []string) { - if len(names) == 0 { - return - } - group := adw.NewPreferencesGroup() - group.SetTitle(title) - page.Add(group) - - var actionRows []*adw.ActionRow - for _, name := range names { - key := cat + ":" + name - row := adw.NewActionRow() - row.SetTitle(name) - sw := gtk.NewSwitch() - sw.SetVAlign(gtk.AlignCenter) - sw.SetActive(w.state[key]) - row.AddSuffix(sw) - row.SetActivatableWidget(sw) - group.Add(row) - w.rows[key] = sw - actionRows = append(actionRows, row) - } - - expanded := true - chevron := gtk.NewImageFromIconName("pan-down-symbolic") - toggleBtn := gtk.NewButton() - toggleBtn.SetChild(chevron) - toggleBtn.AddCSSClass("flat") - toggleBtn.ConnectClicked(func() { - expanded = !expanded - for _, row := range actionRows { - row.SetVisible(expanded) - } - if expanded { - chevron.SetFromIconName("pan-down-symbolic") - } else { - chevron.SetFromIconName("pan-end-symbolic") - } - }) - group.SetHeaderSuffix(toggleBtn) -} - -func (w *mainWin) onApply() { - newState := make(map[string]bool, len(w.rows)) - for k, sw := range w.rows { - newState[k] = sw.Active() - } - - ops := computeOps(w.state, newState, w.compiled, w.binaries) - - if len(ops) == 0 { - dlg := adw.NewAlertDialog("No changes", "Nothing to install or remove.") - dlg.AddResponse("ok", "OK") - dlg.Present(w) - return - } - - w.applyBtn.SetSensitive(false) - - out := newOutputWin(ops, func() { - w.state = newState - saveState(w.state) - w.applyBtn.SetSensitive(true) - }) - out.Present() -} - -func (w *mainWin) onUpdate() { - w.updateBtn.SetSensitive(false) - w.applyBtn.SetSensitive(false) - - ops := []Op{{ - Label: "Run Update", - Cmds: [][]string{{"bash", "-c", "wget -qO- https://gitea.young.computer/david/hampack/raw/branch/main/install.sh | bash"}}, - Env: append(os.Environ(), "TERM=xterm"), - }} - - newOutputWin(ops, func() { - w.updateBtn.SetSensitive(true) - w.applyBtn.SetSensitive(true) - }).Present() -} - -// ── Main ────────────────────────────────────────────────────────────────────── - -func main() { - app := adw.NewApplication("computer.young.HamPack", 0) - app.ConnectActivate(func() { - iconTheme := gtk.IconThemeGetForDisplay(gdk.DisplayGetDefault()) - iconTheme.AddSearchPath(filepath.Join(hampackDir(), "desktop", "icons")) - gtk.WindowSetDefaultIconName("computer.young.HamPack") - newMainWin(app).Present() - }) - os.Exit(app.Run(os.Args)) -} diff --git a/hampackremove b/hampackremove index d3c08bc..f7a97a1 100755 --- a/hampackremove +++ b/hampackremove @@ -64,7 +64,6 @@ echo "Removing HamPack scripts and binaries..." rm -f ~/.local/bin/hampackrefresh rm -f ~/.local/bin/hampackupdate rm -f ~/.local/bin/hampackcheck -rm -f ~/.local/bin/hampack rm -f ~/.local/bin/dx.py # Uninstall not1mm @@ -160,7 +159,6 @@ rm -rf "$HOME/.wine/drive_c/RMS Express" # Remove desktop files echo "Removing desktop files..." -rm -f ~/.local/share/applications/hampack.desktop rm -f ~/.local/share/applications/js8call.desktop rm -f ~/.local/share/applications/yaac.desktop rm -f ~/.local/share/applications/potacat.desktop diff --git a/install.sh b/install.sh index 1b2c5da..b33aa5b 100755 --- a/install.sh +++ b/install.sh @@ -86,30 +86,8 @@ echo "Installing dx.py..." wget -q https://raw.githubusercontent.com/HB9VQQ/dx.py/main/dx.py -O "$HOME/.local/bin/dx.py" chmod +x "$HOME/.local/bin/dx.py" -echo "Building HamPack..." -cd ~/.local/share/HamPack/hampack-src -go get github.com/diamondburned/gotk4-adwaita/pkg/adw@latest -go get github.com/diamondburned/gotk4/pkg/gtk/v4@latest -go mod tidy -go build -o ~/.local/bin/hampack . -cd ~/.local/share/HamPack - -echo "Installing HamPack desktop file..." -mkdir -p ~/.local/share/applications -mkdir -p ~/.local/share/HamPack/desktop/icons/hicolor/256x256/apps -cp ~/.local/share/HamPack/desktop/icons/ham.png \ - ~/.local/share/HamPack/desktop/icons/hicolor/256x256/apps/computer.young.HamPack.png -cat > ~/.local/share/applications/hampack.desktop << EOF -[Desktop Entry] -Type=Application -Name=HamPack -Exec=$HOME/.local/bin/hampack -Icon=$HOME/.local/share/HamPack/desktop/icons/ham.png -Comment=Manage HamPack amateur radio software -Categories=HamRadio; -EOF - echo "Installing JS8Call desktop file..." +mkdir -p ~/.local/share/applications sed "s|\$HOME|$HOME|g" ~/.local/share/HamPack/desktop/js8call.desktop \ > ~/.local/share/applications/js8call.desktop diff --git a/updates.d/170-hampack-build b/updates.d/170-hampack-build deleted file mode 100644 index d2e90f2..0000000 --- a/updates.d/170-hampack-build +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# /etc/coredesktop/updaters.d/170-hampack-build - -HAMPACK_DIR="$HOME/.local/share/HamPack" - -is_installed() { - [[ -d "$HAMPACK_DIR/hampack-src" ]] && command -v go &>/dev/null -} - -run_update() { - echo "→ Rebuilding HamPack..." - cd "$HAMPACK_DIR/hampack-src" - go get github.com/diamondburned/gotk4-adwaita/pkg/adw@latest - go get github.com/diamondburned/gotk4/pkg/gtk/v4@latest - go mod tidy - go build -o "$HOME/.local/bin/hampack" . - cd "$HAMPACK_DIR" - - echo "→ Updating desktop files..." - mkdir -p "$HOME/.local/share/applications" - cat > "$HOME/.local/share/applications/hampack.desktop" << EOF -[Desktop Entry] -Type=Application -Name=HamPack -Exec=$HOME/.local/bin/hampack -Icon=$HOME/.local/share/HamPack/desktop/icons/ham.png -Comment=Manage HamPack amateur radio software -Categories=HamRadio; -EOF - sed "s|\$HOME|$HOME|g" "$HAMPACK_DIR/desktop/js8call.desktop" \ - > "$HOME/.local/share/applications/js8call.desktop" - update-desktop-database "$HOME/.local/share/applications/" - - echo "✓ HamPack built and desktop files updated." -}