kairos-agent/internal/agent/TUImodel.go

303 lines
8.8 KiB
Go
Raw Normal View History

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)
}