kairos-agent/internal/agent/TUIcustomizationPage.go
Itxaka 6d74cdc4b6
Bring over the TUI to interactive installer (#845)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-23 10:54:27 +02:00

208 lines
5.3 KiB
Go

package agent
import (
"encoding/json"
"fmt"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/kairos-io/kairos-agent/v2/internal/bus"
sdk "github.com/kairos-io/kairos-sdk/bus"
"github.com/mudler/go-pluggable"
)
// Customization Page
type YAMLPrompt struct {
YAMLSection string
Bool bool
Prompt string
Default string
AskFirst bool
AskPrompt string
IfEmpty string
PlaceHolder string
}
type EventPayload struct {
Config string `json:"config"`
}
// Discover and run plugins for customization
func runCustomizationPlugins() ([]YAMLPrompt, error) {
bus.Manager.Initialize()
var r []YAMLPrompt
bus.Manager.Response(sdk.EventInteractiveInstall, func(p *pluggable.Plugin, resp *pluggable.EventResponse) {
err := json.Unmarshal([]byte(resp.Data), &r)
if err != nil {
fmt.Println(err)
}
})
_, err := bus.Manager.Publish(sdk.EventInteractiveInstall, EventPayload{})
if err != nil {
return r, err
}
return r, nil
}
func newCustomizationPage() *customizationPage {
return &customizationPage{
options: []string{
"User & Password",
"SSH Keys",
},
cursor: 0,
cursorWithIds: map[int]string{
0: "user_password",
1: "ssh_keys",
},
}
}
func checkPageExists(pageID string, options map[int]string) bool {
for _, opt := range options {
if strings.Contains(opt, pageID) {
return true
}
}
return false
}
type customizationPage struct {
cursor int
options []string
cursorWithIds map[int]string
}
func (p *customizationPage) Title() string {
return "Customization"
}
func (p *customizationPage) Help() string {
return genericNavigationHelp
}
func (p *customizationPage) Init() tea.Cmd {
mainModel.log.Debugf("Running customization plugins...")
yaML, err := runCustomizationPlugins()
if err != nil {
mainModel.log.Debugf("Error running customization plugins: %v", err)
return nil
}
if len(yaML) > 0 {
startIdx := len(p.options)
for i, prompt := range yaML {
// Check if its already added to the options!
if checkPageExists(idFromSection(prompt), p.cursorWithIds) {
mainModel.log.Debugf("Customization page for %s already exists, skipping", prompt.YAMLSection)
continue
}
optIdx := startIdx + i
if prompt.Bool == false {
mainModel.log.Debugf("Adding customization option for %s", prompt.YAMLSection)
p.options = append(p.options, fmt.Sprintf("Configure %s", prompt.YAMLSection))
pageID := idFromSection(prompt)
p.cursorWithIds[optIdx] = pageID
newPage := newGenericQuestionPage(prompt)
mainModel.pages = append(mainModel.pages, newPage)
} else {
mainModel.log.Debugf("Adding customization option(bool) for %s", prompt.YAMLSection)
p.options = append(p.options, fmt.Sprintf("Configure %s", prompt.YAMLSection))
pageID := idFromSection(prompt)
p.cursorWithIds[optIdx] = pageID
newPage := newGenericBoolPage(prompt)
mainModel.pages = append(mainModel.pages, newPage)
}
}
}
// Now add the finish and install options to the bottom of the list
if !checkPageExists("summary", p.cursorWithIds) {
p.options = append(p.options, "Finish Customization and start Installation")
p.cursorWithIds[len(p.cursorWithIds)] = "summary"
}
mainModel.log.Debugf("Customization options loaded: %v", p.cursorWithIds)
return nil
}
func (p *customizationPage) Update(msg tea.Msg) (Page, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "up", "k":
if p.cursor > 0 {
p.cursor--
}
case "down", "j":
if p.cursor < len(p.options)-1 {
p.cursor++
}
case "enter":
if pageID, ok := p.cursorWithIds[p.cursor]; ok {
return p, func() tea.Msg { return GoToPageMsg{PageID: pageID} }
}
}
}
return p, nil
}
func (p *customizationPage) View() string {
s := "Customization Options\n\n"
s += "Configure additional settings:\n\n"
for i, option := range p.options {
cursor := " "
if p.cursor == i {
cursor = lipgloss.NewStyle().Foreground(kairosAccent).Render(">")
}
tick := ""
pageID, ok := p.cursorWithIds[i]
if ok && p.isConfigured(pageID) {
tick = lipgloss.NewStyle().Foreground(kairosAccent).Render(checkMark)
}
s += fmt.Sprintf("%s %s %s\n", cursor, option, tick)
}
return s
}
// Helper methods to check configuration
func (p *customizationPage) isUserConfigured() bool {
return mainModel.username != "" && mainModel.password != ""
}
func (p *customizationPage) isSSHConfigured() bool {
return len(mainModel.sshKeys) > 0
}
func (p *customizationPage) ID() string { return "customization" }
// isConfigured checks if a given pageID is configured, supporting both static and dynamic fields
func (p *customizationPage) isConfigured(pageID string) bool {
// Hardcoded checks for static fields
if pageID == "user_password" {
return p.isUserConfigured()
}
if pageID == "ssh_keys" {
return p.isSSHConfigured()
}
// Try to find a page with this ID and call Configured() if available
for _, page := range mainModel.pages {
if idProvider, ok := page.(interface{ ID() string }); ok && idProvider.ID() == pageID {
// We found the page with the given ID, check if it has a Configured method
if configuredProvider, ok := page.(interface{ Configured() bool }); ok {
// Call the Configured method to check if it's configured
return configuredProvider.Configured()
}
}
}
return false
}