mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-08-20 17:23:50 +00:00
303 lines
8.8 KiB
Go
303 lines
8.8 KiB
Go
|
package agent
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"github.com/kairos-io/kairos-agent/v2/internal/kairos"
|
||
|
|
||
|
"strings"
|
||
|
|
||
|
tea "github.com/charmbracelet/bubbletea"
|
||
|
"github.com/charmbracelet/lipgloss"
|
||
|
"github.com/kairos-io/kairos-sdk/types"
|
||
|
)
|
||
|
|
||
|
// Page interface that all pages must implement
|
||
|
type Page interface {
|
||
|
Init() tea.Cmd
|
||
|
Update(tea.Msg) (Page, tea.Cmd)
|
||
|
View() string
|
||
|
Title() string
|
||
|
Help() string
|
||
|
ID() string // Unique identifier for the page
|
||
|
}
|
||
|
|
||
|
// NextPageMsg is a custom message type for page navigation
|
||
|
type NextPageMsg struct{}
|
||
|
|
||
|
// GoToPageMsg is a custom message type for navigating to a specific page
|
||
|
// Updated to use PageID instead of PageIndex
|
||
|
type GoToPageMsg struct {
|
||
|
PageID string
|
||
|
}
|
||
|
|
||
|
// Main application Model
|
||
|
type Model struct {
|
||
|
pages []Page
|
||
|
currentPageID string // Track current page by ID
|
||
|
navigationStack []string // Stack to track navigation history by ID
|
||
|
width int
|
||
|
height int
|
||
|
title string
|
||
|
disk string // Selected disk
|
||
|
username string
|
||
|
sshKeys []string // Store SSH keys
|
||
|
password string
|
||
|
extraFields map[string]any // Dynamic fields for customization
|
||
|
log *types.KairosLogger
|
||
|
source string // cli flags to interactive installer? what??
|
||
|
|
||
|
showAbortConfirm bool // Show abort confirmation popup
|
||
|
}
|
||
|
|
||
|
var mainModel Model
|
||
|
|
||
|
// InitialModel Initialize the application
|
||
|
func InitialModel(l *types.KairosLogger, source string) Model {
|
||
|
// First create the model with the logger in case any page needs to log something
|
||
|
mainModel = Model{
|
||
|
navigationStack: []string{},
|
||
|
title: kairos.DefaultTitleInteractiveInstaller(),
|
||
|
source: source,
|
||
|
log: l,
|
||
|
}
|
||
|
mainModel.pages = []Page{
|
||
|
newDiskSelectionPage(),
|
||
|
newInstallOptionsPage(),
|
||
|
newCustomizationPage(),
|
||
|
newUserPasswordPage(),
|
||
|
newSSHKeysPage(),
|
||
|
newSummaryPage(),
|
||
|
newInstallProcessPage(),
|
||
|
newUserdataPage(),
|
||
|
}
|
||
|
mainModel.currentPageID = mainModel.pages[0].ID() // Start with the first page
|
||
|
|
||
|
mainModel.log.Logger.Debug().Msg("Initial model created")
|
||
|
return mainModel
|
||
|
}
|
||
|
|
||
|
func (m Model) Init() tea.Cmd {
|
||
|
mainModel.log.Debug("Starting Kairos Interactive Installer")
|
||
|
if len(mainModel.pages) > 0 {
|
||
|
for _, p := range mainModel.pages {
|
||
|
if p.ID() == mainModel.currentPageID {
|
||
|
return p.Init()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
|
mainModel.log.Tracef("Received message: %T", msg)
|
||
|
// Deal with window size changes first
|
||
|
switch msg := msg.(type) {
|
||
|
case tea.WindowSizeMsg:
|
||
|
mainModel.log.Tracef("Window size changed: %dx%d", msg.Width, msg.Height)
|
||
|
mainModel.width = msg.Width
|
||
|
mainModel.height = msg.Height
|
||
|
return m, nil
|
||
|
}
|
||
|
// For navigation, access the mainModel so we can modify from anywhere
|
||
|
currentIdx := -1
|
||
|
for i, p := range mainModel.pages {
|
||
|
if p.ID() == mainModel.currentPageID {
|
||
|
mainModel.log.Tracef("Found current page: %s at index %d", p.ID(), i)
|
||
|
currentIdx = i
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if currentIdx == -1 {
|
||
|
mainModel.log.Error("Current page not found in mainModel.pages")
|
||
|
return mainModel, nil
|
||
|
}
|
||
|
|
||
|
// Hijack all keys if on install process page
|
||
|
if installPage, ok := mainModel.pages[currentIdx].(*installProcessPage); ok {
|
||
|
if mainModel.showAbortConfirm {
|
||
|
// Allow CheckInstallerMsg to update progress even when popup is open
|
||
|
if _, isCheck := msg.(CheckInstallerMsg); isCheck {
|
||
|
updatedPage, cmd := installPage.Update(msg)
|
||
|
mainModel.pages[currentIdx] = updatedPage
|
||
|
return mainModel, cmd
|
||
|
}
|
||
|
// Only handle y/n/esc for popup, block other keys
|
||
|
if keyMsg, isKey := msg.(tea.KeyMsg); isKey {
|
||
|
switch keyMsg.String() {
|
||
|
case "y", "Y":
|
||
|
installPage.Abort()
|
||
|
mainModel.showAbortConfirm = false
|
||
|
fmt.Print("\033[H\033[2J") // Clear the screen before quitting
|
||
|
return mainModel, tea.Quit
|
||
|
case "n", "N", "esc":
|
||
|
mainModel.showAbortConfirm = false
|
||
|
return mainModel, nil
|
||
|
}
|
||
|
}
|
||
|
// Block all other input
|
||
|
return mainModel, nil
|
||
|
}
|
||
|
if keyMsg, isKey := msg.(tea.KeyMsg); isKey {
|
||
|
if keyMsg.Type == tea.KeyCtrlC || keyMsg.String() == "ctrl+c" {
|
||
|
mainModel.showAbortConfirm = true
|
||
|
return mainModel, nil
|
||
|
}
|
||
|
}
|
||
|
if installPage.progress < len(installPage.steps)-1 {
|
||
|
// Ignore all key events during install
|
||
|
if _, isKey := msg.(tea.KeyMsg); isKey {
|
||
|
return mainModel, nil
|
||
|
}
|
||
|
}
|
||
|
if installPage.progress >= len(installPage.steps)-1 {
|
||
|
// After install, any key exits
|
||
|
fmt.Print("\033[H\033[2J") // Clear the screen before quitting
|
||
|
if _, isKey := msg.(tea.KeyMsg); isKey {
|
||
|
return mainModel, tea.Quit
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
mainModel.log.Tracef("Dealing with message in mainModel.Update")
|
||
|
switch msg := msg.(type) {
|
||
|
case tea.KeyMsg:
|
||
|
switch msg.String() {
|
||
|
case "ctrl+c", "q":
|
||
|
mainModel.log.Debug("User requested to quit the installer")
|
||
|
fmt.Print("\033[H\033[2J") // Clear the screen before quitting
|
||
|
return mainModel, tea.Quit
|
||
|
case "esc":
|
||
|
// Go back to previous page if we have navigation history
|
||
|
if len(mainModel.navigationStack) > 0 {
|
||
|
// Pop the last page from the stack
|
||
|
mainModel.currentPageID = mainModel.navigationStack[len(mainModel.navigationStack)-1]
|
||
|
mainModel.navigationStack = mainModel.navigationStack[:len(mainModel.navigationStack)-1]
|
||
|
return mainModel, mainModel.pages[currentIdx].Init()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Handle page navigation
|
||
|
if currentIdx < len(mainModel.pages) {
|
||
|
updatedPage, cmd := mainModel.pages[currentIdx].Update(msg)
|
||
|
mainModel.pages[currentIdx] = updatedPage
|
||
|
|
||
|
// Check if we need to navigate to next page
|
||
|
if _, ok := msg.(NextPageMsg); ok {
|
||
|
if currentIdx < len(mainModel.pages)-1 {
|
||
|
// Push current page to navigation stack
|
||
|
mainModel.navigationStack = append(mainModel.navigationStack, mainModel.currentPageID)
|
||
|
mainModel.currentPageID = mainModel.pages[currentIdx+1].ID()
|
||
|
return mainModel, tea.Batch(cmd, mainModel.pages[currentIdx+1].Init())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Check if we need to navigate to a specific page
|
||
|
if goToPageMsg, ok := msg.(GoToPageMsg); ok {
|
||
|
if goToPageMsg.PageID != "" {
|
||
|
for i, p := range mainModel.pages {
|
||
|
if p.ID() == goToPageMsg.PageID {
|
||
|
mainModel.navigationStack = append(mainModel.navigationStack, mainModel.currentPageID)
|
||
|
mainModel.currentPageID = goToPageMsg.PageID
|
||
|
return mainModel, tea.Batch(cmd, mainModel.pages[i].Init())
|
||
|
}
|
||
|
}
|
||
|
mainModel.log.Tracef("model.Update: pageID=%s not found in mainModel.pages", goToPageMsg.PageID)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return mainModel, cmd
|
||
|
}
|
||
|
|
||
|
return mainModel, nil
|
||
|
}
|
||
|
|
||
|
func (m Model) View() string {
|
||
|
mainModel.log.Tracef("Rendering view for current page: %s", mainModel.currentPageID)
|
||
|
if mainModel.width == 0 || mainModel.height == 0 {
|
||
|
mainModel.log.Debug("Window size is not set, returning loading message")
|
||
|
return "Loading..."
|
||
|
}
|
||
|
|
||
|
borderStyle := lipgloss.NewStyle().
|
||
|
Border(lipgloss.RoundedBorder()).
|
||
|
BorderForeground(kairosBorder).
|
||
|
Background(kairosBg).
|
||
|
Padding(1).
|
||
|
Width(mainModel.width - 4).
|
||
|
Height(mainModel.height - 4)
|
||
|
|
||
|
titleStyle := lipgloss.NewStyle().
|
||
|
Bold(true).
|
||
|
Foreground(kairosHighlight).
|
||
|
Background(kairosBg).
|
||
|
Padding(0, 0).
|
||
|
Width(mainModel.width - 6). // Set width to match content area
|
||
|
Align(lipgloss.Center)
|
||
|
|
||
|
// Get current page content by ID
|
||
|
content := ""
|
||
|
help := ""
|
||
|
for _, p := range mainModel.pages {
|
||
|
if p.ID() == mainModel.currentPageID {
|
||
|
content = p.View()
|
||
|
help = p.Help()
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
title := titleStyle.Render(mainModel.title)
|
||
|
|
||
|
helpStyle := lipgloss.NewStyle().
|
||
|
Foreground(kairosText).
|
||
|
Italic(true)
|
||
|
|
||
|
var fullHelp string
|
||
|
currentIdx := -1
|
||
|
for i, p := range mainModel.pages {
|
||
|
if p.ID() == mainModel.currentPageID {
|
||
|
currentIdx = i
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if currentIdx != -1 {
|
||
|
if _, ok := mainModel.pages[currentIdx].(*installProcessPage); ok {
|
||
|
fullHelp = help
|
||
|
} else if _, ok := mainModel.pages[currentIdx].(*summaryPage); ok {
|
||
|
fullHelp = help
|
||
|
} else if _, ok := mainModel.pages[currentIdx].(*userdataPage); ok {
|
||
|
fullHelp = help
|
||
|
} else {
|
||
|
fullHelp = help + " • ESC: back • q/ctrl+c: quit"
|
||
|
}
|
||
|
}
|
||
|
|
||
|
helpText := helpStyle.Render(fullHelp)
|
||
|
|
||
|
availableHeight := mainModel.height - 8
|
||
|
contentHeight := availableHeight - 2
|
||
|
contentLines := strings.Split(content, "\n")
|
||
|
if len(contentLines) > contentHeight {
|
||
|
contentLines = contentLines[:contentHeight]
|
||
|
content = strings.Join(contentLines, "\n")
|
||
|
}
|
||
|
|
||
|
pageContent := fmt.Sprintf("%s\n\n%s\n\n%s", title, content, helpText)
|
||
|
|
||
|
if mainModel.showAbortConfirm {
|
||
|
mainModel.log.Debug("Showing abort confirmation popup")
|
||
|
popupStyle := lipgloss.NewStyle().
|
||
|
Border(lipgloss.RoundedBorder()).
|
||
|
BorderForeground(kairosAccent).
|
||
|
Background(kairosBg).
|
||
|
Padding(1, 2).
|
||
|
Align(lipgloss.Center)
|
||
|
popupMsg := "Are you sure you want to abort the installation? (y/n)"
|
||
|
popup := popupStyle.Render(popupMsg)
|
||
|
// Overlay the popup in the center
|
||
|
return fmt.Sprintf("%s\n\n%s", borderStyle.Render(pageContent), lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, popup))
|
||
|
}
|
||
|
|
||
|
return borderStyle.Render(pageContent)
|
||
|
}
|