Replace bubbletea TUI with Fyne GUI
All checks were successful
release / build-and-release (push) Successful in 8m25s
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:
parent
8c60b5c74e
commit
0cc4779ed6
6 changed files with 484 additions and 515 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
56
go.mod
|
|
@ -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
127
go.sum
|
|
@ -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
357
internal/gui/gui.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue