mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-09-21 18:27:29 +00:00
Bring over the TUI to interactive installer (#845)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
245
internal/agent/TUIinstallProcessPage.go
Normal file
245
internal/agent/TUIinstallProcessPage.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/kairos-io/kairos-sdk/types"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// Install Process Page
|
||||
type installProcessPage struct {
|
||||
progress int
|
||||
step string
|
||||
steps []string
|
||||
done chan bool // Channel to signal when installation is complete
|
||||
output chan string // Channel to receive output from the installer
|
||||
cmd *exec.Cmd // Reference to the running installer command
|
||||
}
|
||||
|
||||
func newInstallProcessPage() *installProcessPage {
|
||||
return &installProcessPage{
|
||||
progress: 0,
|
||||
step: InstallDefaultStep,
|
||||
steps: []string{
|
||||
InstallDefaultStep,
|
||||
InstallPartitionStep,
|
||||
InstallBeforeInstallStep,
|
||||
InstallActiveStep,
|
||||
InstallBootloaderStep,
|
||||
InstallRecoveryStep,
|
||||
InstallPassiveStep,
|
||||
InstallAfterInstallStep,
|
||||
InstallCompleteStep,
|
||||
},
|
||||
done: make(chan bool),
|
||||
output: make(chan string),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *installProcessPage) Init() tea.Cmd {
|
||||
oldLog := mainModel.log
|
||||
cc := NewInteractiveInstallConfig(&mainModel)
|
||||
// Create a new logger to track the install process output
|
||||
// TODO: Maybe do a dual logger or something? So we can still see the output in the old logger decently
|
||||
logBuffer := bytes.Buffer{}
|
||||
bufferLog := types.NewBufferLogger(&logBuffer)
|
||||
cc.Logger = bufferLog
|
||||
|
||||
// Start the installer in a goroutine
|
||||
go func() {
|
||||
defer close(p.done)
|
||||
err := RunInstall(cc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// Track the log buffer and send mapped steps to p.output
|
||||
go func() {
|
||||
lastLen := 0
|
||||
for {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
buf := logBuffer.Bytes()
|
||||
if len(buf) > lastLen {
|
||||
newLogs := buf[lastLen:]
|
||||
lines := bytes.Split(newLogs, []byte("\n"))
|
||||
for _, line := range lines {
|
||||
strLine := string(line)
|
||||
if len(strLine) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
oldLog.Print(strLine)
|
||||
// Parse log line as JSON and extract the message field
|
||||
var logEntry map[string]interface{}
|
||||
msg := strLine
|
||||
if err := json.Unmarshal([]byte(strLine), &logEntry); err == nil {
|
||||
// Log the message to the old logger still so we have it there
|
||||
if m, ok := logEntry["message"].(string); ok {
|
||||
msg = m
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(msg, AgentPartitionLog) {
|
||||
p.output <- StepPrefix + InstallPartitionStep
|
||||
} else if strings.Contains(msg, AgentBeforeInstallLog) {
|
||||
p.output <- StepPrefix + InstallBeforeInstallStep
|
||||
} else if strings.Contains(msg, AgentActiveLog) {
|
||||
p.output <- StepPrefix + InstallActiveStep
|
||||
} else if strings.Contains(msg, AgentBootloaderLog) {
|
||||
p.output <- StepPrefix + InstallBootloaderStep
|
||||
} else if strings.Contains(msg, AgentRecoveryLog) {
|
||||
p.output <- StepPrefix + InstallRecoveryStep
|
||||
} else if strings.Contains(msg, AgentPassiveLog) {
|
||||
p.output <- StepPrefix + InstallPassiveStep
|
||||
} else if strings.Contains(msg, AgentAfterInstallLog) && !strings.Contains(msg, "chroot") {
|
||||
p.output <- StepPrefix + InstallAfterInstallStep
|
||||
} else if strings.Contains(msg, AgentCompleteLog) {
|
||||
p.output <- StepPrefix + InstallCompleteStep
|
||||
}
|
||||
}
|
||||
lastLen = len(buf)
|
||||
}
|
||||
select {
|
||||
case <-p.done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Return a command that will check for output from the installer
|
||||
return func() tea.Msg {
|
||||
return CheckInstallerMsg{}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckInstallerMsg Message type to check for installer output
|
||||
type CheckInstallerMsg struct{}
|
||||
|
||||
func (p *installProcessPage) Update(msg tea.Msg) (Page, tea.Cmd) {
|
||||
switch msg.(type) {
|
||||
case CheckInstallerMsg:
|
||||
// Check for new output from the installer
|
||||
select {
|
||||
case output, ok := <-p.output:
|
||||
if !ok {
|
||||
// Channel closed, installer is done
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Process the output
|
||||
if strings.HasPrefix(output, StepPrefix) {
|
||||
// This is a step change notification
|
||||
stepName := strings.TrimPrefix(output, StepPrefix)
|
||||
|
||||
// Find the index of the step
|
||||
for i, s := range p.steps {
|
||||
if s == stepName {
|
||||
p.progress = i
|
||||
p.step = stepName
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(output, ErrorPrefix) {
|
||||
// Handle error
|
||||
errorMsg := strings.TrimPrefix(output, ErrorPrefix)
|
||||
p.step = "Error: " + errorMsg
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Continue checking for output
|
||||
return p, func() tea.Msg { return CheckInstallerMsg{} }
|
||||
|
||||
case <-p.done:
|
||||
// Installer is finished
|
||||
p.progress = len(p.steps) - 1
|
||||
p.step = p.steps[len(p.steps)-1]
|
||||
return p, nil
|
||||
|
||||
default:
|
||||
// No new output yet, check again after a short delay
|
||||
return p, tea.Tick(time.Millisecond*100, func(_ time.Time) tea.Msg {
|
||||
return CheckInstallerMsg{}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *installProcessPage) View() string {
|
||||
s := "Installation in Progress\n\n"
|
||||
|
||||
// Progress bar
|
||||
totalSteps := len(p.steps)
|
||||
progressPercent := (p.progress * 100) / (totalSteps - 1)
|
||||
barWidth := 40 // Make progress bar wider
|
||||
filled := barWidth * progressPercent / 100
|
||||
progressBar := lipgloss.NewStyle().Foreground(kairosHighlight2).Background(kairosBg).Render(strings.Repeat("█", filled)) +
|
||||
lipgloss.NewStyle().Foreground(kairosBorder).Background(kairosBg).Render(strings.Repeat("░", barWidth-filled))
|
||||
|
||||
s += "Progress:" + progressBar + lipgloss.NewStyle().Background(kairosBg).Render(" ")
|
||||
s += lipgloss.NewStyle().Foreground(kairosText).Background(kairosBg).Bold(true).Render(fmt.Sprintf("%d%%", progressPercent))
|
||||
s += "\n\n"
|
||||
s += fmt.Sprintf("Current step: %s\n\n", p.step)
|
||||
|
||||
// Show completed steps
|
||||
s += "Completed steps:\n"
|
||||
tick := lipgloss.NewStyle().Foreground(kairosAccent).Render(checkMark)
|
||||
for i := 0; i < p.progress; i++ {
|
||||
s += fmt.Sprintf("%s %s\n", tick, p.steps[i])
|
||||
}
|
||||
|
||||
if p.progress < len(p.steps)-1 {
|
||||
// Make the warning message red
|
||||
warning := lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")).Bold(true).Render("[!] Do not power off the system during installation!")
|
||||
s += "\n" + warning
|
||||
} else {
|
||||
// Make the completion message green
|
||||
complete := lipgloss.NewStyle().Foreground(lipgloss.Color("#00FF00")).Bold(true).Render("Installation completed successfully!\nYou can now reboot your system.")
|
||||
s += "\n" + complete
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *installProcessPage) Title() string {
|
||||
return "Installing"
|
||||
}
|
||||
|
||||
func (p *installProcessPage) Help() string {
|
||||
if p.progress >= len(p.steps)-1 {
|
||||
return "Press any key to exit"
|
||||
}
|
||||
return "Installation in progress - Use ctrl+c to abort"
|
||||
}
|
||||
|
||||
func (p *installProcessPage) ID() string { return "install_process" }
|
||||
|
||||
// Abort aborts the running installer process and cleans up
|
||||
func (p *installProcessPage) Abort() {
|
||||
if p.cmd != nil && p.cmd.Process != nil {
|
||||
_ = p.cmd.Process.Kill()
|
||||
mainModel.log.Info("Installer process aborted by user")
|
||||
}
|
||||
// Close output channel if not already closed
|
||||
select {
|
||||
case <-p.done:
|
||||
// already closed
|
||||
default:
|
||||
close(p.done)
|
||||
}
|
||||
// Optionally, send a message to output channel
|
||||
select {
|
||||
case p.output <- ErrorPrefix + "Installation aborted by user":
|
||||
default:
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user