735 lines
20 KiB
Go
735 lines
20 KiB
Go
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
tea "github.com/charmbracelet/bubbletea"
|
||
"github.com/charmbracelet/x/ansi"
|
||
)
|
||
|
||
var tuiVersion = "source"
|
||
|
||
type selection struct {
|
||
Name string `json:"name"`
|
||
Profile string `json:"profile"`
|
||
Groups []string `json:"groups"`
|
||
Configs []string `json:"configs"`
|
||
Kernel string `json:"kernel"`
|
||
Bootloader string `json:"bootloader"`
|
||
Extras []string `json:"extras"`
|
||
Firefox bool `json:"firefox_privacy"`
|
||
Spotify bool `json:"spotify_custom"`
|
||
Flatpaks []string `json:"flatpaks"`
|
||
Services []string `json:"services"`
|
||
}
|
||
type config struct {
|
||
Label string `json:"label"`
|
||
Group string `json:"group"`
|
||
}
|
||
type setupOption struct {
|
||
Label string `json:"label"`
|
||
Group string `json:"group"`
|
||
Scope string `json:"scope"`
|
||
}
|
||
|
||
type catalog struct {
|
||
Groups []string `json:"groups"`
|
||
Configs map[string]config `json:"configs"`
|
||
Extras map[string]string `json:"extras"`
|
||
Flatpaks map[string]setupOption `json:"flatpaks"`
|
||
Services map[string]setupOption `json:"services"`
|
||
Stock selection `json:"stock"`
|
||
Ryzen selection `json:"ryzen"`
|
||
}
|
||
type row struct {
|
||
id, label string
|
||
checked bool
|
||
}
|
||
type previewMsg struct {
|
||
content string
|
||
err error
|
||
}
|
||
type backupChoice struct {
|
||
ID string `json:"id"`
|
||
Count int `json:"count"`
|
||
}
|
||
type backupsMsg struct {
|
||
choices []backupChoice
|
||
err error
|
||
}
|
||
type proposalChoice struct {
|
||
ID string `json:"id"`
|
||
Target string `json:"target"`
|
||
Status string `json:"status"`
|
||
}
|
||
type proposalsMsg struct {
|
||
choices []proposalChoice
|
||
err error
|
||
}
|
||
type model struct {
|
||
repo string
|
||
catalog catalog
|
||
selection selection
|
||
rows []row
|
||
stage, cursor, height, width, scroll int
|
||
preview, errorText, action, backupID string
|
||
done, loading bool
|
||
proposalID, proposalStatus string
|
||
proposals []proposalChoice
|
||
}
|
||
|
||
func contains(values []string, target string) bool {
|
||
for _, value := range values {
|
||
if value == target {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
func (m model) Init() tea.Cmd { return nil }
|
||
func (m *model) prepare() {
|
||
m.cursor, m.scroll = 0, 0
|
||
m.rows = nil
|
||
switch m.stage {
|
||
case 0:
|
||
m.rows = []row{{"stock", "Stock — binary kernel, portable build settings", m.selection.Profile == "stock"}, {"fiw-ryzen", "Fiw's Ryzen — current Zen 5 tuning, custom + binary fallback", m.selection.Profile == "fiw-ryzen"}}
|
||
if m.selection.Profile == "fiw-ryzen" {
|
||
m.cursor = 1
|
||
}
|
||
case 1:
|
||
for _, id := range m.catalog.Groups {
|
||
label := id
|
||
if id == "tidewm" {
|
||
label += " (optional)"
|
||
}
|
||
m.rows = append(m.rows, row{id, label, contains(m.selection.Groups, id)})
|
||
}
|
||
case 2:
|
||
ids := make([]string, 0, len(m.catalog.Configs))
|
||
for id := range m.catalog.Configs {
|
||
if id != "autostart" {
|
||
ids = append(ids, id)
|
||
}
|
||
}
|
||
sort.Strings(ids)
|
||
for _, id := range ids {
|
||
c := m.catalog.Configs[id]
|
||
m.rows = append(m.rows, row{id, c.Label, contains(m.selection.Configs, id)})
|
||
}
|
||
case 3:
|
||
m.rows = []row{
|
||
{"binary", "Kernel: generic binary", m.selection.Kernel == "binary"},
|
||
{"custom", "Kernel: custom + generic binary fallback", m.selection.Kernel == "custom"},
|
||
{"keep", "Bootloader: keep existing", m.selection.Bootloader == "keep"},
|
||
{"limine", "Bootloader: optional Limine", m.selection.Bootloader == "limine"},
|
||
{"grub", "Bootloader: optional GRUB", m.selection.Bootloader == "grub"},
|
||
}
|
||
for _, id := range []string{"nvidia", "btrfs", "filelight"} {
|
||
m.rows = append(m.rows, row{id, m.catalog.Extras[id], contains(m.selection.Extras, id)})
|
||
}
|
||
m.rows = append(m.rows, row{"autostart", "Optional app autostart", contains(m.selection.Configs, "autostart")}, row{"firefox", "Optional Firefox-Privacy setup", m.selection.Firefox}, row{"spotify", "Optional Spotify customization script (stock by default)", m.selection.Spotify})
|
||
case 4:
|
||
flatpaks := make([]string, 0, len(m.catalog.Flatpaks))
|
||
for id := range m.catalog.Flatpaks {
|
||
flatpaks = append(flatpaks, id)
|
||
}
|
||
sort.Strings(flatpaks)
|
||
for _, id := range flatpaks {
|
||
option := m.catalog.Flatpaks[id]
|
||
m.rows = append(m.rows, row{"flatpak:" + id, "Flatpak: " + option.Label + " (" + option.Group + ")", contains(m.selection.Flatpaks, id)})
|
||
}
|
||
for _, id := range []string{"audio", "network", "bluetooth", "power"} {
|
||
if option, ok := m.catalog.Services[id]; ok {
|
||
m.rows = append(m.rows, row{"service:" + id, option.Label, contains(m.selection.Services, id)})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
func (m *model) remember() {
|
||
switch m.stage {
|
||
case 1:
|
||
m.selection.Groups = []string{}
|
||
for _, r := range m.rows {
|
||
if r.checked {
|
||
m.selection.Groups = append(m.selection.Groups, r.id)
|
||
}
|
||
}
|
||
case 2:
|
||
autostart := contains(m.selection.Configs, "autostart")
|
||
m.selection.Configs = []string{}
|
||
for _, r := range m.rows {
|
||
if r.checked {
|
||
m.selection.Configs = append(m.selection.Configs, r.id)
|
||
}
|
||
}
|
||
if autostart {
|
||
m.selection.Configs = append(m.selection.Configs, "autostart")
|
||
}
|
||
case 3:
|
||
m.selection.Extras = []string{}
|
||
for _, r := range m.rows {
|
||
if _, ok := m.catalog.Extras[r.id]; ok && r.checked {
|
||
m.selection.Extras = append(m.selection.Extras, r.id)
|
||
}
|
||
if r.checked && (r.id == "binary" || r.id == "custom") {
|
||
m.selection.Kernel = r.id
|
||
}
|
||
if r.checked && (r.id == "keep" || r.id == "limine" || r.id == "grub") {
|
||
m.selection.Bootloader = r.id
|
||
}
|
||
if r.id == "firefox" {
|
||
m.selection.Firefox = r.checked
|
||
}
|
||
if r.id == "spotify" {
|
||
m.selection.Spotify = r.checked
|
||
}
|
||
}
|
||
configNames := []string{}
|
||
for _, id := range m.selection.Configs {
|
||
if id != "autostart" {
|
||
configNames = append(configNames, id)
|
||
}
|
||
}
|
||
for _, r := range m.rows {
|
||
if r.id == "autostart" && r.checked {
|
||
configNames = append(configNames, "autostart")
|
||
}
|
||
}
|
||
m.selection.Configs = configNames
|
||
case 4:
|
||
m.selection.Flatpaks, m.selection.Services = []string{}, []string{}
|
||
for _, r := range m.rows {
|
||
if !r.checked {
|
||
continue
|
||
}
|
||
if strings.HasPrefix(r.id, "flatpak:") {
|
||
m.selection.Flatpaks = append(m.selection.Flatpaks, strings.TrimPrefix(r.id, "flatpak:"))
|
||
}
|
||
if strings.HasPrefix(r.id, "service:") {
|
||
m.selection.Services = append(m.selection.Services, strings.TrimPrefix(r.id, "service:"))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
func (m model) makePreview() tea.Cmd {
|
||
return func() tea.Msg {
|
||
file, err := os.CreateTemp("", "fiw-dots-selection-*.json")
|
||
if err != nil {
|
||
return previewMsg{err: err}
|
||
}
|
||
defer os.Remove(file.Name())
|
||
data, err := json.Marshal(m.selection)
|
||
if err == nil {
|
||
_, err = file.Write(data)
|
||
}
|
||
file.Close()
|
||
if err != nil {
|
||
return previewMsg{err: err}
|
||
}
|
||
cmd := exec.Command("python3", filepath.Join(m.repo, "lib", "rice.py"), "--plan", "--selection", file.Name())
|
||
output, err := cmd.CombinedOutput()
|
||
return previewMsg{string(output), err}
|
||
}
|
||
}
|
||
func (m model) loadBackups() tea.Cmd {
|
||
return func() tea.Msg {
|
||
output, err := exec.Command("python3", filepath.Join(m.repo, "lib", "rice.py"), "--list-backups").CombinedOutput()
|
||
var choices []backupChoice
|
||
if err == nil {
|
||
err = json.Unmarshal(output, &choices)
|
||
} else {
|
||
err = fmt.Errorf("%s", strings.TrimSpace(string(output)))
|
||
}
|
||
return backupsMsg{choices, err}
|
||
}
|
||
}
|
||
func (m model) backupPreview() tea.Cmd {
|
||
return func() tea.Msg {
|
||
output, err := exec.Command("python3", filepath.Join(m.repo, "lib", "rice.py"), "--backup-plan", m.backupID).CombinedOutput()
|
||
return previewMsg{string(output), err}
|
||
}
|
||
}
|
||
func (m model) loadProposals() tea.Cmd {
|
||
return func() tea.Msg {
|
||
output, err := exec.Command("python3", filepath.Join(m.repo, "lib", "rice.py"), "--list-proposals").CombinedOutput()
|
||
var choices []proposalChoice
|
||
if err == nil {
|
||
err = json.Unmarshal(output, &choices)
|
||
} else {
|
||
err = fmt.Errorf("%s", strings.TrimSpace(string(output)))
|
||
}
|
||
return proposalsMsg{choices, err}
|
||
}
|
||
}
|
||
func (m model) proposalPreview() tea.Cmd {
|
||
return func() tea.Msg {
|
||
output, err := exec.Command("python3", filepath.Join(m.repo, "lib", "rice.py"), "--proposal-plan", m.proposalID).CombinedOutput()
|
||
return previewMsg{string(output), err}
|
||
}
|
||
}
|
||
func (m model) previewLines() []string {
|
||
width := m.width
|
||
if width == 0 {
|
||
width = 80
|
||
}
|
||
if width < 10 {
|
||
width = 10
|
||
}
|
||
return strings.Split(ansi.Wrap(m.preview, width-4, ""), "\n")
|
||
}
|
||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
switch v := msg.(type) {
|
||
case tea.WindowSizeMsg:
|
||
m.height = v.Height
|
||
m.width = v.Width
|
||
if m.scroll >= len(m.previewLines()) {
|
||
m.scroll = len(m.previewLines()) - 1
|
||
}
|
||
case previewMsg:
|
||
m.loading = false
|
||
m.preview = v.content
|
||
if m.stage == 9 && v.err == nil {
|
||
lines := strings.SplitN(v.content, "\n", 4)
|
||
m.proposalStatus = ""
|
||
if len(lines) > 2 {
|
||
m.proposalStatus = strings.TrimPrefix(lines[2], "Status: ")
|
||
}
|
||
}
|
||
if v.err != nil {
|
||
m.errorText = v.err.Error()
|
||
}
|
||
case backupsMsg:
|
||
m.loading = false
|
||
m.rows = nil
|
||
if v.err != nil {
|
||
m.errorText = v.err.Error()
|
||
}
|
||
for _, choice := range v.choices {
|
||
m.rows = append(m.rows, row{choice.ID, fmt.Sprintf("%s — %d files", choice.ID, choice.Count), false})
|
||
}
|
||
case proposalsMsg:
|
||
m.loading = false
|
||
m.rows = nil
|
||
m.proposals = v.choices
|
||
if v.err != nil {
|
||
m.errorText = v.err.Error()
|
||
}
|
||
for _, choice := range v.choices {
|
||
m.rows = append(m.rows, row{choice.ID, choice.ID + " [" + choice.Status + "]", false})
|
||
}
|
||
case tea.KeyMsg:
|
||
key := v.String()
|
||
if key == "ctrl+c" || key == "q" {
|
||
return m, tea.Quit
|
||
}
|
||
if m.loading {
|
||
return m, nil
|
||
}
|
||
if m.stage == 5 || m.stage == 7 || m.stage == 9 {
|
||
switch key {
|
||
case "up", "k":
|
||
if m.scroll > 0 {
|
||
m.scroll--
|
||
}
|
||
case "down", "j":
|
||
if m.scroll < len(m.previewLines())-1 {
|
||
m.scroll++
|
||
}
|
||
case "esc":
|
||
m.stage--
|
||
m.prepare()
|
||
if m.stage == 6 {
|
||
m.loading = true
|
||
m.errorText = ""
|
||
return m, m.loadBackups()
|
||
}
|
||
if m.stage == 8 {
|
||
m.loading = true
|
||
m.errorText = ""
|
||
return m, m.loadProposals()
|
||
}
|
||
case "u":
|
||
if m.stage == 5 {
|
||
m.stage = 6
|
||
m.prepare()
|
||
m.loading = true
|
||
m.errorText = ""
|
||
return m, m.loadBackups()
|
||
}
|
||
case "p":
|
||
if m.stage == 5 {
|
||
m.stage = 8
|
||
m.prepare()
|
||
m.loading = true
|
||
m.errorText = ""
|
||
return m, m.loadProposals()
|
||
}
|
||
case "enter", "s", "a", "e", "i", "f", "v", "b", "r":
|
||
if m.errorText == "" {
|
||
if m.stage == 7 {
|
||
if key != "enter" {
|
||
return m, nil
|
||
}
|
||
key = "u"
|
||
}
|
||
if m.stage == 9 {
|
||
if key != "enter" || (m.proposalStatus != "ready" && m.proposalStatus != "unchanged") {
|
||
return m, nil
|
||
}
|
||
key = "p"
|
||
}
|
||
m.done = true
|
||
m.action = key
|
||
return m, tea.Quit
|
||
}
|
||
}
|
||
return m, nil
|
||
}
|
||
if m.stage == 6 || m.stage == 8 {
|
||
switch key {
|
||
case "up", "k":
|
||
if m.cursor > 0 {
|
||
m.cursor--
|
||
}
|
||
case "down", "j":
|
||
if m.cursor < len(m.rows)-1 {
|
||
m.cursor++
|
||
}
|
||
case "enter":
|
||
if len(m.rows) > 0 && m.errorText == "" {
|
||
if m.stage == 8 {
|
||
m.proposalID = m.rows[m.cursor].id
|
||
m.proposalStatus = m.proposals[m.cursor].Status
|
||
m.stage = 9
|
||
m.scroll = 0
|
||
m.loading = true
|
||
return m, m.proposalPreview()
|
||
}
|
||
m.backupID = m.rows[m.cursor].id
|
||
m.stage = 7
|
||
m.scroll = 0
|
||
m.loading = true
|
||
return m, m.backupPreview()
|
||
}
|
||
case "esc":
|
||
m.stage = 5
|
||
m.scroll = 0
|
||
m.errorText = ""
|
||
m.loading = true
|
||
return m, m.makePreview()
|
||
}
|
||
return m, nil
|
||
}
|
||
switch key {
|
||
case "up", "k":
|
||
if m.cursor > 0 {
|
||
m.cursor--
|
||
}
|
||
case "down", "j":
|
||
if m.cursor < len(m.rows)-1 {
|
||
m.cursor++
|
||
}
|
||
case " ":
|
||
if len(m.rows) == 0 {
|
||
break
|
||
}
|
||
if m.stage == 0 {
|
||
if m.rows[m.cursor].id == "stock" {
|
||
m.selection = m.catalog.Stock
|
||
} else {
|
||
m.selection = m.catalog.Ryzen
|
||
}
|
||
m.prepare()
|
||
} else {
|
||
id := m.rows[m.cursor].id
|
||
if m.stage == 3 && (id == "binary" || id == "custom") {
|
||
for i := range m.rows {
|
||
if m.rows[i].id == "binary" || m.rows[i].id == "custom" {
|
||
m.rows[i].checked = false
|
||
}
|
||
}
|
||
m.rows[m.cursor].checked = true
|
||
} else if m.stage == 3 && (id == "keep" || id == "limine" || id == "grub") {
|
||
for i := range m.rows {
|
||
if m.rows[i].id == "keep" || m.rows[i].id == "limine" || m.rows[i].id == "grub" {
|
||
m.rows[i].checked = false
|
||
}
|
||
}
|
||
m.rows[m.cursor].checked = true
|
||
} else {
|
||
m.rows[m.cursor].checked = !m.rows[m.cursor].checked
|
||
}
|
||
}
|
||
case "enter":
|
||
if m.stage == 0 {
|
||
if m.rows[m.cursor].id == "stock" {
|
||
m.selection = m.catalog.Stock
|
||
} else {
|
||
m.selection = m.catalog.Ryzen
|
||
}
|
||
}
|
||
m.remember()
|
||
m.stage++
|
||
if m.stage == 5 {
|
||
m.loading = true
|
||
m.errorText = ""
|
||
return m, m.makePreview()
|
||
}
|
||
m.prepare()
|
||
case "esc":
|
||
if m.stage > 0 {
|
||
m.remember()
|
||
m.stage--
|
||
m.prepare()
|
||
}
|
||
}
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m model) View() string {
|
||
const purple = "\033[38;2;157;143;217m"
|
||
const reset = "\033[0m"
|
||
titles := []string{"Preset", "Package sections", "Optional app configs", "Kernel, bootloader and extras", "Flatpaks and optional services", "Final preview", "Config backups", "Restore backup preview", "Pending config updates", "Config update diff"}
|
||
text := purple + " Fiw-Gentoo-Dots\n" + reset + " " + titles[m.stage] + " · " + m.selection.Name + "\n\n"
|
||
budget := m.height - 10
|
||
if budget < 5 {
|
||
budget = 5
|
||
}
|
||
if m.stage == 5 || m.stage == 7 || m.stage == 9 {
|
||
if m.loading {
|
||
return text + " Preparing preview…\n"
|
||
}
|
||
lines := m.previewLines()
|
||
end := m.scroll + budget
|
||
if end > len(lines) {
|
||
end = len(lines)
|
||
}
|
||
for _, line := range lines[m.scroll:end] {
|
||
text += " " + line + "\n"
|
||
}
|
||
if m.stage == 7 {
|
||
text += "\n ↑/↓ scroll · Esc back · Enter restore with confirmation · q cancel\n"
|
||
} else if m.stage == 9 {
|
||
if m.proposalStatus == "stale" {
|
||
text += "\n Target changed: update configs again to create a fresh proposal.\n ↑/↓ scroll · Esc back · q cancel\n"
|
||
} else {
|
||
text += "\n ↑/↓ scroll · Esc back · Enter accept with confirmation · q cancel\n"
|
||
}
|
||
} else {
|
||
text += "\n ↑/↓ scroll · Esc back · Enter save · r full restore · a configs · e update configs\n i packages · f Flatpaks · v services · b bootloader\n p pending updates · u config backups · q cancel\n"
|
||
}
|
||
if m.errorText != "" {
|
||
text += " " + m.errorText + "\n"
|
||
}
|
||
return text
|
||
}
|
||
if (m.stage == 6 || m.stage == 8) && (m.loading || len(m.rows) == 0 || m.errorText != "") {
|
||
message := "No config backups yet. Backups are created when existing configs are replaced."
|
||
if m.stage == 8 {
|
||
message = "No pending config updates. Use e from the final preview to update configs."
|
||
}
|
||
if m.loading {
|
||
message = "Loading files…"
|
||
}
|
||
if m.errorText != "" {
|
||
message = m.errorText
|
||
}
|
||
return text + " " + message + "\n\n Esc back · q cancel\n"
|
||
}
|
||
start := 0
|
||
if m.cursor >= budget {
|
||
start = m.cursor - budget + 1
|
||
}
|
||
end := start + budget
|
||
if end > len(m.rows) {
|
||
end = len(m.rows)
|
||
}
|
||
for i := start; i < end; i++ {
|
||
r := m.rows[i]
|
||
mark := "[ ]"
|
||
if r.checked {
|
||
mark = "[✓]"
|
||
}
|
||
cursor := " "
|
||
if m.cursor == i {
|
||
cursor = "› "
|
||
}
|
||
line := " " + cursor + mark + " " + r.label
|
||
if m.stage == 8 && m.width > 10 {
|
||
line = ansi.Truncate(line, m.width-2, "…")
|
||
}
|
||
if m.cursor == i {
|
||
line = purple + line + reset
|
||
}
|
||
text += line + "\n"
|
||
}
|
||
if m.stage == 6 {
|
||
return text + "\n ↑/↓ move · Enter preview backup · Esc back · q cancel\n"
|
||
}
|
||
if m.stage == 8 {
|
||
return text + "\n ↑/↓ move · Enter preview diff · Esc back · q cancel\n"
|
||
}
|
||
return text + "\n ↑/↓ move · Space select · Enter next · Esc back · q cancel\n"
|
||
}
|
||
|
||
func main() {
|
||
if len(os.Args) == 2 && os.Args[1] == "--version" {
|
||
fmt.Println("Fiw-Gentoo-Dots TUI " + tuiVersion)
|
||
return
|
||
}
|
||
repo, err := os.Getwd()
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, err)
|
||
os.Exit(1)
|
||
}
|
||
output, err := exec.Command("python3", filepath.Join(repo, "lib", "rice.py"), "--catalog").Output()
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, "Cannot load the installer catalogue:", err)
|
||
os.Exit(1)
|
||
}
|
||
var c catalog
|
||
if err := json.Unmarshal(output, &c); err != nil {
|
||
fmt.Fprintln(os.Stderr, err)
|
||
os.Exit(1)
|
||
}
|
||
m := model{repo: repo, catalog: c, selection: c.Stock, height: 28}
|
||
savedPath := filepath.Join(repo, "local", "selection.json")
|
||
if saved, readErr := os.ReadFile(savedPath); readErr == nil {
|
||
var previous selection
|
||
if json.Unmarshal(saved, &previous) == nil && (previous.Profile == "stock" || previous.Profile == "fiw-ryzen") {
|
||
m.selection = previous
|
||
m.stage = 1
|
||
}
|
||
}
|
||
m.prepare()
|
||
result, err := tea.NewProgram(m, tea.WithAltScreen()).Run()
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, err)
|
||
os.Exit(1)
|
||
}
|
||
final := result.(model)
|
||
if !final.done {
|
||
return
|
||
}
|
||
path := filepath.Join(repo, "local", "selection.json")
|
||
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
||
fmt.Fprintln(os.Stderr, err)
|
||
os.Exit(1)
|
||
}
|
||
data, _ := json.MarshalIndent(final.selection, "", " ")
|
||
if err := os.WriteFile(path, append(data, '\n'), 0600); err != nil {
|
||
fmt.Fprintln(os.Stderr, err)
|
||
os.Exit(1)
|
||
}
|
||
fmt.Println(final.preview)
|
||
fmt.Println("\nSaved selection:", path)
|
||
runID := fmt.Sprintf("%s-%d", time.Now().UTC().Format("20060102T150405.000000000Z"), os.Getpid())
|
||
run := func(action string, root bool, extra ...string) error {
|
||
args := []string{filepath.Join(repo, "lib", "rice.py"), "--selection", path, "--run-id", runID, action}
|
||
args = append(args, extra...)
|
||
program := "python3"
|
||
if root {
|
||
program = "sudo"
|
||
args = append([]string{"python3"}, args...)
|
||
}
|
||
cmd := exec.Command(program, args...)
|
||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||
return cmd.Run()
|
||
}
|
||
services := func() error {
|
||
system, user := false, false
|
||
for _, id := range final.selection.Services {
|
||
if c.Services[id].Scope == "system" {
|
||
system = true
|
||
}
|
||
if c.Services[id].Scope == "user" {
|
||
user = true
|
||
}
|
||
}
|
||
if system {
|
||
if err := run("--enable-services", true); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if user {
|
||
return run("--enable-services", false)
|
||
}
|
||
if !system {
|
||
fmt.Println("No services selected.")
|
||
}
|
||
return nil
|
||
}
|
||
var actionErr error
|
||
workflow := "restore"
|
||
switch final.action {
|
||
case "a":
|
||
workflow = "configs"
|
||
actionErr = run("--apply-configs", false)
|
||
case "e":
|
||
workflow = "configs"
|
||
actionErr = run("--apply-configs", false, "--update")
|
||
case "p":
|
||
workflow = "proposal"
|
||
actionErr = run("--accept-proposal", false, final.proposalID)
|
||
case "i":
|
||
workflow = "packages"
|
||
actionErr = run("--install-packages", true)
|
||
case "f":
|
||
workflow = "flatpaks"
|
||
actionErr = run("--install-flatpaks", false)
|
||
case "v":
|
||
workflow = "services"
|
||
actionErr = services()
|
||
case "b":
|
||
workflow = "boot"
|
||
actionErr = run("--deploy-bootloader", true)
|
||
case "u":
|
||
workflow = "backup"
|
||
actionErr = run("--restore-backup", false, final.backupID)
|
||
case "r":
|
||
actionErr = run("--check-configs", false)
|
||
if actionErr == nil && (len(final.selection.Groups) > 0 || len(final.selection.Extras) > 0 || final.selection.Bootloader != "keep" || len(final.selection.Flatpaks) > 0 || len(final.selection.Services) > 0) {
|
||
actionErr = run("--install-packages", true)
|
||
}
|
||
if actionErr == nil && len(final.selection.Flatpaks) > 0 {
|
||
actionErr = run("--install-flatpaks", false)
|
||
}
|
||
if actionErr == nil && len(final.selection.Services) > 0 {
|
||
actionErr = services()
|
||
}
|
||
if actionErr == nil && len(final.selection.Configs) > 0 {
|
||
actionErr = run("--apply-configs", false)
|
||
}
|
||
if actionErr == nil && final.selection.Bootloader != "keep" {
|
||
actionErr = run("--deploy-bootloader", true)
|
||
}
|
||
default:
|
||
fmt.Println("Saved. Choose an action in the TUI or use the commands in docs/setup.md.")
|
||
return
|
||
}
|
||
summaryArgs := []string{"--workflow", workflow}
|
||
if actionErr != nil {
|
||
summaryArgs = append(summaryArgs, "--execution-error", actionErr.Error())
|
||
}
|
||
if summaryErr := run("--summary", false, summaryArgs...); summaryErr != nil {
|
||
fmt.Fprintln(os.Stderr, "Cannot create the combined report:", summaryErr)
|
||
}
|
||
if actionErr != nil {
|
||
var exitError *exec.ExitError
|
||
if errors.As(actionErr, &exitError) && exitError.ExitCode() == 130 {
|
||
fmt.Println("Cancelled. Completed steps remain recorded in the summary.")
|
||
os.Exit(130)
|
||
}
|
||
fmt.Fprintln(os.Stderr, actionErr)
|
||
os.Exit(1)
|
||
}
|
||
}
|