diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 787f8d2..a801a50 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -14,7 +14,7 @@ on: jobs: 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 defaults: run: @@ -25,7 +25,7 @@ jobs: - name: Setup working-directory: / run: | - apk add --no-cache git curl jq + apk add --no-cache git curl jq mingw-w64-gcc git clone --depth=1 \ "http://x9:${{ secrets.FORGEJO_TOKEN }}@xetup-forgejo:3000/${{ github.repository }}.git" \ /repo @@ -34,7 +34,9 @@ jobs: - name: Build xetup.exe 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}')" - name: Publish latest release diff --git a/cmd/xetup/main.go b/cmd/xetup/main.go index ad80707..fb81787 100644 --- a/cmd/xetup/main.go +++ b/cmd/xetup/main.go @@ -1,11 +1,14 @@ -// Command xetup is the interactive TUI 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. +// Command xetup is the GUI launcher for Windows deployment. // -// 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 import ( @@ -14,35 +17,31 @@ import ( "os" "path/filepath" - tea "github.com/charmbracelet/bubbletea" - xetupembed "git.xetup.x9.cz/x9/xetup" "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/tui" ) 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() cfg, err := config.Load(cfgPath) if err != nil { 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-*") if err != nil { log.Fatalf("Cannot create temp dir: %v", err) } defer os.RemoveAll(tmpDir) - // Extract embedded scripts + // Extract embedded scripts and assets if err := runner.ExtractScripts(xetupembed.Scripts, tmpDir); err != nil { log.Fatalf("Failed to extract scripts: %v", err) } - - // Extract embedded assets if err := runner.ExtractAssets(xetupembed.Assets, tmpDir); err != nil { log.Fatalf("Failed to extract assets: %v", err) } @@ -60,11 +59,5 @@ func main() { ProfileType: cfg.Deployment.ProfileType, } - // Launch the TUI - m := tui.NewModel(cfg, runCfg) - p := tea.NewProgram(m, tea.WithAltScreen()) - if _, err := p.Run(); err != nil { - log.Fatalf("TUI error: %v", err) - } + gui.Run(cfg, runCfg, cfgPath) } -// build 1776332628 diff --git a/go.mod b/go.mod index 7acf944..a56bcb6 100644 --- a/go.mod +++ b/go.mod @@ -2,31 +2,39 @@ module git.xetup.x9.cz/x9/xetup go 1.24.0 +require fyne.io/fyne/v2 v2.7.3 + require ( - github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/catppuccin/go v0.3.0 // indirect - github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect - github.com/charmbracelet/bubbletea v1.3.10 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/huh v1.0.0 // indirect - github.com/charmbracelet/lipgloss v1.1.0 // indirect - github.com/charmbracelet/x/ansi v0.10.1 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13 // indirect - github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + fyne.io/systray v1.12.0 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fredbi/uri v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fyne-io/gl-js v0.2.0 // indirect + github.com/fyne-io/glfw-js v0.3.0 // indirect + github.com/fyne-io/image v0.1.1 // indirect + github.com/fyne-io/oksvg v0.2.0 // indirect + github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect + github.com/go-text/render v0.2.0 // indirect + github.com/go-text/typesetting v0.3.3 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/hack-pad/go-indexeddb v0.3.2 // indirect + github.com/hack-pad/safejs v0.1.0 // indirect + github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect + github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rymdport/portal v0.4.2 // 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/text v0.23.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 235f2a6..07c7fae 100644 --- a/go.sum +++ b/go.sum @@ -1,55 +1,80 @@ -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= -github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= -github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= -github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw= -github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= -github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= -github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= -github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= -github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= -github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= -github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= -github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +fyne.io/fyne/v2 v2.7.3 h1:xBT/iYbdnNHONWO38fZMBrVBiJG8rV/Jypmy4tVfRWE= +fyne.io/fyne/v2 v2.7.3/go.mod h1:gu+dlIcZWSzKZmnrY8Fbnj2Hirabv2ek+AKsfQ2bBlw= +fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM= +fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko= +github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs= +github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI= +github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk= +github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk= +github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA= +github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM= +github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8= +github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI= +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc= +github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU= +github.com/go-text/typesetting v0.3.3 h1:ihGNJU9KzdK2QRDy1Bm7FT5RFQoYb+3n3EIhI/4eaQc= +github.com/go-text/typesetting v0.3.3/go.mod h1:vIRUT25mLQaSh4C8H/lIsKppQz/Gdb8Pu/tNwpi52ts= +github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8 h1:4KCscI9qYWMGTuz6BpJtbUSRzcBrUSSE0ENMJbNSrFs= +github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A= +github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= +github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= +github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= +github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE= +github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= +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/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 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= diff --git a/internal/gui/gui.go b/internal/gui/gui.go new file mode 100644 index 0000000..5fe6928 --- /dev/null +++ b/internal/gui/gui.go @@ -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 +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go deleted file mode 100644 index 58c6e00..0000000 --- a/internal/tui/tui.go +++ /dev/null @@ -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 -}