Replace bubbletea TUI with Fyne GUI
All checks were successful
release / build-and-release (push) Successful in 8m25s

- Drop bubbletea, huh, lipgloss and all their transitive deps
- Add fyne.io/fyne/v2 – native Windows GUI, dark theme
- New internal/gui/gui.go: 3-phase window (form → live run → summary)
  - Form: PC name, product key, profile, per-step checkboxes
  - Load config / Save config buttons for per-client presets
  - SPUSTIT button auto-saves to default config.json
  - Live run: virtualised log list, ZASTAVIT button
  - Summary: per-step status + elapsed time, ZAVRIT button
- cmd/xetup/main.go: pass cfgPath to gui.Run so save/load works
- CI: add mingw-w64-gcc, CGO_ENABLED=1, -H windowsgui flag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
X9 Dev 2026-04-16 12:29:38 +02:00
parent 8c60b5c74e
commit 0cc4779ed6
6 changed files with 484 additions and 515 deletions

View file

@ -14,7 +14,7 @@ on:
jobs: jobs:
build-and-release: build-and-release:
# Runner label 'ubuntu-latest' maps to golang:1.23-alpine container (see runner config) # Runner label 'ubuntu-latest' maps to golang:1.24-alpine container (see runner config)
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults: defaults:
run: run:
@ -25,7 +25,7 @@ jobs:
- name: Setup - name: Setup
working-directory: / working-directory: /
run: | run: |
apk add --no-cache git curl jq apk add --no-cache git curl jq mingw-w64-gcc
git clone --depth=1 \ git clone --depth=1 \
"http://x9:${{ secrets.FORGEJO_TOKEN }}@xetup-forgejo:3000/${{ github.repository }}.git" \ "http://x9:${{ secrets.FORGEJO_TOKEN }}@xetup-forgejo:3000/${{ github.repository }}.git" \
/repo /repo
@ -34,7 +34,9 @@ jobs:
- name: Build xetup.exe - name: Build xetup.exe
run: | run: |
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o xetup.exe ./cmd/xetup/ CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc \
GOOS=windows GOARCH=amd64 \
go build -ldflags="-s -w -H windowsgui" -o xetup.exe ./cmd/xetup/
echo "Built: $(ls -lh xetup.exe | awk '{print $5}')" echo "Built: $(ls -lh xetup.exe | awk '{print $5}')"
- name: Publish latest release - name: Publish latest release

View file

@ -1,11 +1,14 @@
// Command xetup is the interactive TUI launcher for Windows deployment. // Command xetup is the GUI launcher for Windows deployment.
// It embeds all PowerShell scripts and assets into a single self-contained
// binary, extracts them to a temp directory at runtime, collects configuration
// via an interactive form, and streams live log output while the scripts run.
// //
// Cross-compile for Windows: // All PowerShell scripts and assets are embedded in the binary and extracted
// to a temp directory at runtime. The GUI collects configuration, streams
// live log output while the scripts run, and shows a final summary.
// //
// GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o xetup.exe ./cmd/xetup // Cross-compile for Windows (requires MinGW):
//
// CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc \
// GOOS=windows GOARCH=amd64 \
// go build -ldflags="-s -w -H windowsgui" -o xetup.exe ./cmd/xetup
package main package main
import ( import (
@ -14,35 +17,31 @@ import (
"os" "os"
"path/filepath" "path/filepath"
tea "github.com/charmbracelet/bubbletea"
xetupembed "git.xetup.x9.cz/x9/xetup" xetupembed "git.xetup.x9.cz/x9/xetup"
"git.xetup.x9.cz/x9/xetup/internal/config" "git.xetup.x9.cz/x9/xetup/internal/config"
"git.xetup.x9.cz/x9/xetup/internal/gui"
"git.xetup.x9.cz/x9/xetup/internal/runner" "git.xetup.x9.cz/x9/xetup/internal/runner"
"git.xetup.x9.cz/x9/xetup/internal/tui"
) )
func main() { func main() {
// Load config (silently falls back to defaults when missing) // Load config (falls back to defaults when config.json is missing)
cfgPath := config.ConfigPath() cfgPath := config.ConfigPath()
cfg, err := config.Load(cfgPath) cfg, err := config.Load(cfgPath)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Warning: config load failed (%v), using defaults\n", err) fmt.Fprintf(os.Stderr, "Warning: config load failed (%v), using defaults\n", err)
} }
// Create a temp working directory; cleaned up on exit // Temp working directory cleaned up on exit
tmpDir, err := os.MkdirTemp("", "xetup-*") tmpDir, err := os.MkdirTemp("", "xetup-*")
if err != nil { if err != nil {
log.Fatalf("Cannot create temp dir: %v", err) log.Fatalf("Cannot create temp dir: %v", err)
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
// Extract embedded scripts // Extract embedded scripts and assets
if err := runner.ExtractScripts(xetupembed.Scripts, tmpDir); err != nil { if err := runner.ExtractScripts(xetupembed.Scripts, tmpDir); err != nil {
log.Fatalf("Failed to extract scripts: %v", err) log.Fatalf("Failed to extract scripts: %v", err)
} }
// Extract embedded assets
if err := runner.ExtractAssets(xetupembed.Assets, tmpDir); err != nil { if err := runner.ExtractAssets(xetupembed.Assets, tmpDir); err != nil {
log.Fatalf("Failed to extract assets: %v", err) log.Fatalf("Failed to extract assets: %v", err)
} }
@ -60,11 +59,5 @@ func main() {
ProfileType: cfg.Deployment.ProfileType, ProfileType: cfg.Deployment.ProfileType,
} }
// Launch the TUI gui.Run(cfg, runCfg, cfgPath)
m := tui.NewModel(cfg, runCfg)
p := tea.NewProgram(m, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
log.Fatalf("TUI error: %v", err)
}
} }
// build 1776332628

56
go.mod
View file

@ -2,31 +2,39 @@ module git.xetup.x9.cz/x9/xetup
go 1.24.0 go 1.24.0
require fyne.io/fyne/v2 v2.7.3
require ( require (
github.com/atotto/clipboard v0.1.4 // indirect fyne.io/systray v1.12.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/BurntSushi/toml v1.5.0 // indirect
github.com/catppuccin/go v0.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect github.com/fredbi/uri v1.1.1 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/fyne-io/gl-js v0.2.0 // indirect
github.com/charmbracelet/huh v1.0.0 // indirect github.com/fyne-io/glfw-js v0.3.0 // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/fyne-io/image v0.1.1 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect github.com/fyne-io/oksvg v0.2.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/go-text/render v0.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-text/typesetting v0.3.3 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/hack-pad/safejs v0.1.0 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/kr/text v0.2.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/muesli/termenv v0.16.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rymdport/portal v0.4.2 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.36.0 // indirect golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

127
go.sum
View file

@ -1,55 +1,80 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= fyne.io/fyne/v2 v2.7.3 h1:xBT/iYbdnNHONWO38fZMBrVBiJG8rV/Jypmy4tVfRWE=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= fyne.io/fyne/v2 v2.7.3/go.mod h1:gu+dlIcZWSzKZmnrY8Fbnj2Hirabv2ek+AKsfQ2bBlw=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw= github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/go-text/typesetting v0.3.3 h1:ihGNJU9KzdK2QRDy1Bm7FT5RFQoYb+3n3EIhI/4eaQc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/go-text/typesetting v0.3.3/go.mod h1:vIRUT25mLQaSh4C8H/lIsKppQz/Gdb8Pu/tNwpi52ts=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8 h1:4KCscI9qYWMGTuz6BpJtbUSRzcBrUSSE0ENMJbNSrFs=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

357
internal/gui/gui.go Normal file
View file

@ -0,0 +1,357 @@
// Package gui implements the Fyne-based graphical interface for xetup.
//
// Three phases, one window:
// 1. Config form PC name, product key, profile, step selection,
// load/save config buttons for per-client presets
// 2. Live run real-time log streamed from PowerShell scripts
// 3. Summary per-step OK / ERROR / SKIPPED with elapsed time
package gui
import (
"context"
"encoding/json"
"fmt"
"io"
"sync"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"git.xetup.x9.cz/x9/xetup/internal/config"
"git.xetup.x9.cz/x9/xetup/internal/runner"
)
// Run opens the xetup window and blocks until the user closes it.
// cfgPath is the default config.json path (next to the exe).
func Run(cfg config.Config, runCfg runner.RunConfig, cfgPath string) {
a := app.New()
a.Settings().SetTheme(theme.DarkTheme())
w := a.NewWindow("xetup — Windows deployment")
w.Resize(fyne.NewSize(740, 680))
w.SetMaster() // closing this window quits the app
showForm(w, cfg, runCfg, cfgPath)
w.ShowAndRun()
}
// --------------------------------------------------------------------------
// Phase 1 Config form
// --------------------------------------------------------------------------
func showForm(w fyne.Window, cfg config.Config, runCfg runner.RunConfig, cfgPath string) {
// ── Text inputs ─────────────────────────────────────────────────────────
pcName := widget.NewEntry()
pcName.SetPlaceHolder("napr. NB-KLIENT-01 (prazdne = neprejmenovat)")
pcName.SetText(cfg.Deployment.PCName)
pcDesc := widget.NewEntry()
pcDesc.SetPlaceHolder("napr. PC recepce")
pcDesc.SetText(cfg.Deployment.PCDescription)
productKey := widget.NewEntry()
productKey.SetPlaceHolder("prazdne = OA3 / GVLK fallback")
productKey.SetText(cfg.Activation.ProductKey)
profileSel := widget.NewSelect([]string{"default", "admin", "user"}, nil)
profileSel.SetSelected(cfg.Deployment.ProfileType)
// ── Step checkboxes ─────────────────────────────────────────────────────
items := runner.AllSelectableItems()
checks := make([]*widget.Check, len(items))
checkObjs := make([]fyne.CanvasObject, len(items))
for i, item := range items {
c := widget.NewCheck(item.Label, nil)
c.SetChecked(itemEnabled(cfg, item))
checks[i] = c
checkObjs[i] = c
}
stepsScroll := container.NewVScroll(container.NewVBox(checkObjs...))
stepsScroll.SetMinSize(fyne.NewSize(0, 290))
// ── collectCfg reads current form state into a Config ───────────────────
collectCfg := func() config.Config {
out := cfg // start from loaded config (preserves fields not shown in form)
out.Deployment.PCName = pcName.Text
out.Deployment.PCDescription = pcDesc.Text
out.Activation.ProductKey = productKey.Text
out.Deployment.ProfileType = profileSel.Selected
selected := make(map[string]bool, len(items))
for i, item := range items {
selected[item.Key] = checks[i].Checked
}
_, features := buildStepsAndFeatures(selected)
out.Features = features
return out
}
// ── Toolbar: Load / Save config ─────────────────────────────────────────
jsonFilter := storage.NewExtensionFileFilter([]string{".json"})
loadBtn := widget.NewButton("Nacist config...", func() {
d := dialog.NewFileOpen(func(rc fyne.URIReadCloser, err error) {
if err != nil || rc == nil {
return
}
defer rc.Close()
data, err := io.ReadAll(rc)
if err != nil {
dialog.ShowError(err, w)
return
}
newCfg := config.DefaultConfig()
if err := json.Unmarshal(data, &newCfg); err != nil {
dialog.ShowError(err, w)
return
}
// Reload the entire form with the new config
showForm(w, newCfg, runCfg, rc.URI().Path())
}, w)
d.SetFilter(jsonFilter)
d.Show()
})
saveBtn := widget.NewButton("Ulozit config...", func() {
d := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) {
if err != nil || wc == nil {
return
}
defer wc.Close()
data, err := json.MarshalIndent(collectCfg(), "", " ")
if err != nil {
dialog.ShowError(err, w)
return
}
if _, err := wc.Write(data); err != nil {
dialog.ShowError(err, w)
}
}, w)
d.SetFilter(jsonFilter)
d.SetFileName("config.json")
d.Show()
})
// ── SPUSTIT ─────────────────────────────────────────────────────────────
startBtn := widget.NewButton(" SPUSTIT ", func() {
finalCfg := collectCfg()
runCfg.ProfileType = finalCfg.Deployment.ProfileType
selected := make(map[string]bool, len(items))
for i, item := range items {
selected[item.Key] = checks[i].Checked
}
steps, features := buildStepsAndFeatures(selected)
finalCfg.Features = features
_ = config.Save(finalCfg, cfgPath) // auto-save to default path
showRun(w, runCfg, steps)
})
startBtn.Importance = widget.HighImportance
// ── Layout ───────────────────────────────────────────────────────────────
form := widget.NewForm(
widget.NewFormItem("PC jmeno", pcName),
widget.NewFormItem("Popis PC", pcDesc),
widget.NewFormItem("Product Key", productKey),
widget.NewFormItem("Profil", profileSel),
)
toolbar := container.NewHBox(loadBtn, saveBtn)
w.SetContent(container.NewBorder(
form,
container.NewVBox(
widget.NewSeparator(),
container.NewBorder(nil, nil, toolbar, container.NewCenter(startBtn)),
),
nil, nil,
container.NewVBox(
widget.NewSeparator(),
widget.NewLabel("Kroky a nastaveni (odskrtnete co nechcete spustit):"),
stepsScroll,
),
))
}
// --------------------------------------------------------------------------
// Phase 2 Live run view
// --------------------------------------------------------------------------
func showRun(w fyne.Window, runCfg runner.RunConfig, steps []runner.Step) {
statusLabel := widget.NewLabel("Spoustim...")
// Virtualised list efficient for thousands of log lines
var (
mu sync.Mutex
logLines []string
)
logList := widget.NewList(
func() int {
mu.Lock()
defer mu.Unlock()
return len(logLines)
},
func() fyne.CanvasObject {
return widget.NewLabel("")
},
func(id widget.ListItemID, obj fyne.CanvasObject) {
mu.Lock()
defer mu.Unlock()
if id < len(logLines) {
obj.(*widget.Label).SetText(logLines[id])
}
},
)
var cancelFn context.CancelFunc
stopBtn := widget.NewButton(" ZASTAVIT ", func() {
if cancelFn != nil {
cancelFn()
}
})
stopBtn.Importance = widget.DangerImportance
w.SetContent(container.NewBorder(
container.NewVBox(statusLabel, widget.NewSeparator()),
container.NewCenter(container.NewPadded(stopBtn)),
nil, nil,
logList,
))
ctx, cancel := context.WithCancel(context.Background())
cancelFn = cancel
r := runner.New(
runCfg,
func(l runner.LogLine) {
mu.Lock()
logLines = append(logLines, l.Text)
mu.Unlock()
logList.Refresh()
logList.ScrollToBottom()
},
func(res runner.Result) {
statusLabel.SetText(fmt.Sprintf(
"Krok %s %s: %s", res.Step.Num, res.Step.Name, res.Status,
))
},
)
go func() {
results := r.Run(ctx, steps)
showDone(w, results)
}()
}
// --------------------------------------------------------------------------
// Phase 3 Summary
// --------------------------------------------------------------------------
func showDone(w fyne.Window, results []runner.Result) {
ok, errs, skipped := 0, 0, 0
rows := make([]fyne.CanvasObject, 0, len(results))
for _, res := range results {
var icon string
switch res.Status {
case "OK":
ok++
icon = "OK "
case "ERROR":
errs++
icon = "ERR "
default:
skipped++
icon = " "
}
text := icon + res.Step.Num + " " + res.Step.Name
if res.Elapsed > 0 {
text += fmt.Sprintf(" (%s)", res.Elapsed.Round(time.Second))
}
rows = append(rows, widget.NewLabel(text))
}
summary := widget.NewLabel(fmt.Sprintf(
"OK: %d CHYBY: %d PRESKOCENO: %d", ok, errs, skipped,
))
summary.TextStyle = fyne.TextStyle{Bold: true}
closeBtn := widget.NewButton(" ZAVRIT ", func() {
fyne.CurrentApp().Quit()
})
w.SetContent(container.NewBorder(
widget.NewLabelWithStyle("Hotovo", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
container.NewVBox(
widget.NewSeparator(),
container.NewCenter(summary),
container.NewCenter(container.NewPadded(closeBtn)),
),
nil, nil,
container.NewVScroll(container.NewVBox(rows...)),
))
}
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
// itemEnabled returns the initial checked state for a checkbox row,
// reading from the loaded config (defaults to true / enabled when absent).
func itemEnabled(cfg config.Config, item runner.SelectableItem) bool {
if item.FeatureID == "" {
if v, ok := cfg.Steps[item.StepID]; ok {
return v
}
return true
}
if feats, ok := cfg.Features[item.StepID]; ok {
if v, ok2 := feats[item.FeatureID]; ok2 {
return v
}
}
return true
}
// buildStepsAndFeatures converts the flat checkbox map into the structures
// that runner.Runner and config.Config expect.
func buildStepsAndFeatures(selected map[string]bool) ([]runner.Step, config.Features) {
items := runner.AllSelectableItems()
features := make(config.Features)
stepOn := make(map[string]bool)
for _, item := range items {
if item.FeatureID == "" {
// Simple step: enabled iff its own checkbox is checked
stepOn[item.StepID] = selected[item.Key]
} else {
// Feature checkbox: at least one checked feature enables the step
if features[item.StepID] == nil {
features[item.StepID] = make(map[string]bool)
}
features[item.StepID][item.FeatureID] = selected[item.Key]
if selected[item.Key] {
stepOn[item.StepID] = true
}
}
}
allSteps := runner.AllSteps()
steps := make([]runner.Step, len(allSteps))
for i, s := range allSteps {
s.Enabled = stepOn[s.ID]
steps[i] = s
}
return steps, features
}

View file

@ -1,416 +0,0 @@
// Package tui implements the interactive terminal UI for xetup.
// Phase 1 configuration form (huh embedded in bubbletea).
// Phase 2 live log view while PowerShell scripts run.
// Phase 3 final summary.
package tui
import (
"context"
"fmt"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"git.xetup.x9.cz/x9/xetup/internal/config"
"git.xetup.x9.cz/x9/xetup/internal/runner"
)
// --------------------------------------------------------------------------
// Phase
// --------------------------------------------------------------------------
type phase int
const (
phaseForm phase = iota
phaseRun
phaseDone
)
// --------------------------------------------------------------------------
// Async message types (runner → bubbletea)
// --------------------------------------------------------------------------
type logMsg runner.LogLine
type resultMsg runner.Result
type doneMsg []runner.Result
// --------------------------------------------------------------------------
// Model
// --------------------------------------------------------------------------
// Model is the top-level bubbletea model.
type Model struct {
phase phase
form *huh.Form
runCfg runner.RunConfig
cfg config.Config
// form-bound values (huh writes here via pointer)
pcName string
pcDesc string
productKey string
profileType string
selectedItemKeys []string
// runner state
r *runner.Runner
cancel context.CancelFunc
msgCh chan tea.Msg
logs []runner.LogLine
results []runner.Result
// terminal dimensions
width int
height int
}
// NewModel returns the initial model pre-populated from cfg.
func NewModel(cfg config.Config, runCfg runner.RunConfig) Model {
m := Model{
cfg: cfg,
runCfg: runCfg,
pcName: cfg.Deployment.PCName,
pcDesc: cfg.Deployment.PCDescription,
productKey: cfg.Activation.ProductKey,
profileType: cfg.Deployment.ProfileType,
width: 80,
height: 24,
}
m.form = buildForm(&m)
return m
}
// --------------------------------------------------------------------------
// Form construction
// --------------------------------------------------------------------------
func buildForm(m *Model) *huh.Form {
allItems := runner.AllSelectableItems()
opts := make([]huh.Option[string], len(allItems))
for i, item := range allItems {
opts[i] = huh.NewOption(item.Label, item.Key).Selected(true)
}
return huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Jmeno pocitace").
Placeholder("napr. NB-KLIENT-01 (prazdne = neprejmenovat)").
Value(&m.pcName),
huh.NewInput().
Title("Popis pocitace").
Placeholder("napr. PC recepce").
Value(&m.pcDesc),
huh.NewInput().
Title("Product Key").
Placeholder("prazdne = OA3 / GVLK fallback").
Value(&m.productKey),
huh.NewSelect[string]().
Title("Profil").
Options(
huh.NewOption("Default", "default"),
huh.NewOption("Admin (Explorer, PS, Edge)", "admin"),
huh.NewOption("User (Explorer, Edge)", "user"),
).
Value(&m.profileType),
),
huh.NewGroup(
huh.NewMultiSelect[string]().
Title("Kroky a nastaveni (mezernikem odzaskrtnout)").
Options(opts...).
Value(&m.selectedItemKeys),
),
)
}
// --------------------------------------------------------------------------
// bubbletea interface
// --------------------------------------------------------------------------
// Init implements tea.Model.
func (m Model) Init() tea.Cmd {
return m.form.Init()
}
// Update implements tea.Model.
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
if m.cancel != nil {
m.cancel()
}
return m, tea.Quit
}
if m.phase == phaseDone {
return m, tea.Quit
}
// --- async runner messages ---
case logMsg:
m.logs = append(m.logs, runner.LogLine(msg))
return m, readMsg(m.msgCh)
case resultMsg:
m.results = append(m.results, runner.Result(msg))
return m, readMsg(m.msgCh)
case doneMsg:
m.results = []runner.Result(msg)
m.phase = phaseDone
return m, nil
}
// --- form phase ---
if m.phase == phaseForm {
form, cmd := m.form.Update(msg)
if f, ok := form.(*huh.Form); ok {
m.form = f
}
switch m.form.State {
case huh.StateCompleted:
return m.startRun()
case huh.StateAborted:
return m, tea.Quit
}
return m, cmd
}
return m, nil
}
// View implements tea.Model.
func (m Model) View() string {
switch m.phase {
case phaseForm:
return m.viewForm()
case phaseRun:
return m.viewRun()
case phaseDone:
return m.viewDone()
}
return ""
}
// --------------------------------------------------------------------------
// Phase transitions
// --------------------------------------------------------------------------
// startRun applies form values and kicks off the runner goroutine.
func (m Model) startRun() (Model, tea.Cmd) {
// persist form values into config
m.cfg.Deployment.PCName = m.pcName
m.cfg.Deployment.PCDescription = m.pcDesc
m.cfg.Activation.ProductKey = m.productKey
m.cfg.Deployment.ProfileType = m.profileType
m.runCfg.ProfileType = m.profileType
// Index selected keys for fast lookup
selectedSet := make(map[string]bool, len(m.selectedItemKeys))
for _, key := range m.selectedItemKeys {
selectedSet[key] = true
}
// Build Features map: feature enabled = its composite key was selected
features := make(config.Features)
for _, item := range runner.AllSelectableItems() {
if item.FeatureID == "" {
continue
}
if features[item.StepID] == nil {
features[item.StepID] = make(map[string]bool)
}
features[item.StepID][item.FeatureID] = selectedSet[item.Key]
}
m.cfg.Features = features
// Determine which steps are enabled:
// - step with features: enabled if at least one feature selected
// - step without features: enabled if step key selected
stepEnabled := make(map[string]bool)
for _, item := range runner.AllSelectableItems() {
if item.FeatureID == "" {
stepEnabled[item.StepID] = selectedSet[item.Key]
} else if selectedSet[item.Key] {
// any feature selected enables the step
stepEnabled[item.StepID] = true
}
}
allSteps := runner.AllSteps()
steps := make([]runner.Step, len(allSteps))
for i, s := range allSteps {
s.Enabled = stepEnabled[s.ID]
steps[i] = s
}
// set up channel and start runner
ch := make(chan tea.Msg, 200)
m.msgCh = ch
m.phase = phaseRun
ctx, cancel := context.WithCancel(context.Background())
m.cancel = cancel
rc := m.runCfg
r := runner.New(
rc,
func(l runner.LogLine) { ch <- logMsg(l) },
func(res runner.Result) { ch <- resultMsg(res) },
)
m.r = r
go func() {
results := r.Run(ctx, steps)
ch <- doneMsg(results)
}()
return m, readMsg(ch)
}
// readMsg returns a tea.Cmd that blocks until the next message arrives on ch.
func readMsg(ch <-chan tea.Msg) tea.Cmd {
return func() tea.Msg {
return <-ch
}
}
// --------------------------------------------------------------------------
// Styles
// --------------------------------------------------------------------------
var (
sTitle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#58a6ff"))
sOK = lipgloss.NewStyle().Foreground(lipgloss.Color("#2ea043"))
sError = lipgloss.NewStyle().Foreground(lipgloss.Color("#da3633"))
sWarn = lipgloss.NewStyle().Foreground(lipgloss.Color("#d29922"))
sStep = lipgloss.NewStyle().Foreground(lipgloss.Color("#58a6ff"))
sMuted = lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
sBold = lipgloss.NewStyle().Bold(true)
)
// --------------------------------------------------------------------------
// Views
// --------------------------------------------------------------------------
func (m Model) viewForm() string {
header := sTitle.Render("xetup") + " " + sMuted.Render("Windows deployment vyplnte konfiguraci")
return header + "\n\n" + m.form.View()
}
func (m Model) viewRun() string {
allSteps := runner.AllSteps()
doneCount := len(m.results)
totalCount := len(allSteps)
var sb strings.Builder
// header bar
sb.WriteString(sTitle.Render("xetup") + " ")
if doneCount < totalCount {
s := allSteps[doneCount]
sb.WriteString(sBold.Render(fmt.Sprintf("krok %d/%d", doneCount+1, totalCount)))
sb.WriteString(" " + s.Num + " - " + s.Name)
} else {
sb.WriteString(sBold.Render("dokoncuji..."))
}
sb.WriteByte('\n')
sb.WriteString(strings.Repeat("─", clamp(m.width, 20, 90)))
sb.WriteByte('\n')
// log lines show last N to fit terminal
maxLines := m.height - 5
if maxLines < 5 {
maxLines = 5
}
start := 0
if len(m.logs) > maxLines {
start = len(m.logs) - maxLines
}
for _, l := range m.logs[start:] {
sb.WriteString(styledLogLine(l))
sb.WriteByte('\n')
}
sb.WriteString(sMuted.Render("\nCtrl+C pro zastaveni"))
return sb.String()
}
func (m Model) viewDone() string {
var sb strings.Builder
sb.WriteString(sTitle.Render("xetup ") + sBold.Render("hotovo") + "\n")
sb.WriteString(strings.Repeat("─", clamp(m.width, 20, 90)) + "\n\n")
okCount, errCount, skipCount := 0, 0, 0
for _, res := range m.results {
var prefix string
switch res.Status {
case "OK":
okCount++
prefix = sOK.Render(" OK ")
case "ERROR":
errCount++
prefix = sError.Render(" ERROR ")
default:
skipCount++
prefix = sMuted.Render(" SKIPPED ")
}
sb.WriteString(prefix + res.Step.Num + " - " + res.Step.Name)
if res.Elapsed > 0 {
sb.WriteString(sMuted.Render(fmt.Sprintf(" (%s)", res.Elapsed.Round(time.Second))))
}
sb.WriteByte('\n')
}
sb.WriteString("\n" + strings.Repeat("─", clamp(m.width, 20, 90)) + "\n")
summary := fmt.Sprintf("OK: %d CHYBY: %d PRESKOCENO: %d", okCount, errCount, skipCount)
if errCount > 0 {
sb.WriteString(sError.Render(summary))
} else {
sb.WriteString(sOK.Render(summary))
}
sb.WriteString(sMuted.Render("\n\nStisknete libovolnou klavesu pro ukonceni..."))
return sb.String()
}
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
func styledLogLine(l runner.LogLine) string {
switch l.Level {
case "OK":
return sOK.Render(l.Text)
case "ERROR":
return sError.Render(l.Text)
case "WARN":
return sWarn.Render(l.Text)
case "STEP":
return sStep.Render(l.Text)
default:
return sMuted.Render(l.Text)
}
}
func clamp(v, lo, hi int) int {
if v < lo {
return lo
}
if v > hi {
return hi
}
return v
}