mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-11 07:52:50 -05:00
rename backend to core
This commit is contained in:
244
core/internal/tui/app.go
Normal file
244
core/internal/tui/app.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
version string
|
||||
state ApplicationState
|
||||
|
||||
osInfo *distros.OSInfo
|
||||
dependencies []deps.Dependency
|
||||
err error
|
||||
|
||||
spinner spinner.Model
|
||||
passwordInput textinput.Model
|
||||
width int
|
||||
height int
|
||||
isLoading bool
|
||||
styles Styles
|
||||
|
||||
logMessages []string
|
||||
logChan chan string
|
||||
logFilePath string
|
||||
packageProgressChan chan packageInstallProgressMsg
|
||||
packageProgress packageInstallProgressMsg
|
||||
installationLogs []string
|
||||
showDebugLogs bool
|
||||
|
||||
selectedWM int
|
||||
selectedTerminal int
|
||||
selectedDep int
|
||||
selectedConfig int
|
||||
reinstallItems map[string]bool
|
||||
disabledItems map[string]bool
|
||||
replaceConfigs map[string]bool
|
||||
skipGentooUseFlags bool
|
||||
sudoPassword string
|
||||
existingConfigs []ExistingConfigInfo
|
||||
fingerprintFailed bool
|
||||
}
|
||||
|
||||
func NewModel(version string, logFilePath string) Model {
|
||||
s := spinner.New()
|
||||
s.Spinner = spinner.Dot
|
||||
|
||||
theme := TerminalTheme()
|
||||
styles := NewStyles(theme)
|
||||
s.Style = styles.SpinnerStyle
|
||||
|
||||
pi := textinput.New()
|
||||
pi.Placeholder = "Enter sudo password"
|
||||
pi.EchoMode = textinput.EchoPassword
|
||||
pi.EchoCharacter = '•'
|
||||
pi.Focus()
|
||||
|
||||
logChan := make(chan string, 1000)
|
||||
packageProgressChan := make(chan packageInstallProgressMsg, 100)
|
||||
|
||||
return Model{
|
||||
version: version,
|
||||
state: StateWelcome,
|
||||
spinner: s,
|
||||
passwordInput: pi,
|
||||
isLoading: true,
|
||||
styles: styles,
|
||||
|
||||
logMessages: []string{},
|
||||
logChan: logChan,
|
||||
logFilePath: logFilePath,
|
||||
packageProgressChan: packageProgressChan,
|
||||
packageProgress: packageInstallProgressMsg{
|
||||
progress: 0.0,
|
||||
step: "Initializing package installation",
|
||||
isComplete: false,
|
||||
},
|
||||
showDebugLogs: false,
|
||||
selectedWM: 0,
|
||||
selectedTerminal: 0, // Default to Ghostty
|
||||
selectedDep: 0,
|
||||
selectedConfig: 0,
|
||||
reinstallItems: make(map[string]bool),
|
||||
disabledItems: make(map[string]bool),
|
||||
replaceConfigs: make(map[string]bool),
|
||||
installationLogs: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) GetLogChan() <-chan string {
|
||||
return m.logChan
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
m.spinner.Tick,
|
||||
m.listenForLogs(),
|
||||
m.detectOS(),
|
||||
)
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
switch keyMsg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "ctrl+d":
|
||||
// Toggle debug logs view (except during password input states)
|
||||
if m.state != StatePasswordPrompt && m.state != StateFingerprintAuth {
|
||||
m.showDebugLogs = !m.showDebugLogs
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tickMsg, ok := msg.(spinner.TickMsg); ok {
|
||||
var cmd tea.Cmd
|
||||
m.spinner, cmd = m.spinner.Update(tickMsg)
|
||||
return m, tea.Batch(cmd, m.listenForLogs())
|
||||
}
|
||||
|
||||
if sizeMsg, ok := msg.(tea.WindowSizeMsg); ok {
|
||||
m.width = sizeMsg.Width
|
||||
m.height = sizeMsg.Height
|
||||
}
|
||||
|
||||
if logMsg, ok := msg.(logMsg); ok {
|
||||
m.logMessages = append(m.logMessages, logMsg.message)
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
|
||||
switch m.state {
|
||||
case StateWelcome:
|
||||
return m.updateWelcomeState(msg)
|
||||
case StateSelectWindowManager:
|
||||
return m.updateSelectWindowManagerState(msg)
|
||||
case StateSelectTerminal:
|
||||
return m.updateSelectTerminalState(msg)
|
||||
case StateMissingWMInstructions:
|
||||
return m.updateMissingWMInstructionsState(msg)
|
||||
case StateDetectingDeps:
|
||||
return m.updateDetectingDepsState(msg)
|
||||
case StateDependencyReview:
|
||||
return m.updateDependencyReviewState(msg)
|
||||
case StateGentooUseFlags:
|
||||
return m.updateGentooUseFlagsState(msg)
|
||||
case StateGentooGCCCheck:
|
||||
return m.updateGentooGCCCheckState(msg)
|
||||
case StateAuthMethodChoice:
|
||||
return m.updateAuthMethodChoiceState(msg)
|
||||
case StateFingerprintAuth:
|
||||
return m.updateFingerprintAuthState(msg)
|
||||
case StatePasswordPrompt:
|
||||
return m.updatePasswordPromptState(msg)
|
||||
case StateInstallingPackages:
|
||||
return m.updateInstallingPackagesState(msg)
|
||||
case StateConfigConfirmation:
|
||||
return m.updateConfigConfirmationState(msg)
|
||||
case StateDeployingConfigs:
|
||||
return m.updateDeployingConfigsState(msg)
|
||||
case StateInstallComplete:
|
||||
return m.updateInstallCompleteState(msg)
|
||||
case StateError:
|
||||
return m.updateErrorState(msg)
|
||||
default:
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
// If debug logs are shown, show that view regardless of state
|
||||
if m.showDebugLogs {
|
||||
return m.viewDebugLogs()
|
||||
}
|
||||
|
||||
switch m.state {
|
||||
case StateWelcome:
|
||||
return m.viewWelcome()
|
||||
case StateSelectWindowManager:
|
||||
return m.viewSelectWindowManager()
|
||||
case StateSelectTerminal:
|
||||
return m.viewSelectTerminal()
|
||||
case StateMissingWMInstructions:
|
||||
return m.viewMissingWMInstructions()
|
||||
case StateDetectingDeps:
|
||||
return m.viewDetectingDeps()
|
||||
case StateDependencyReview:
|
||||
return m.viewDependencyReview()
|
||||
case StateGentooUseFlags:
|
||||
return m.viewGentooUseFlags()
|
||||
case StateGentooGCCCheck:
|
||||
return m.viewGentooGCCCheck()
|
||||
case StateAuthMethodChoice:
|
||||
return m.viewAuthMethodChoice()
|
||||
case StateFingerprintAuth:
|
||||
return m.viewFingerprintAuth()
|
||||
case StatePasswordPrompt:
|
||||
return m.viewPasswordPrompt()
|
||||
case StateInstallingPackages:
|
||||
return m.viewInstallingPackages()
|
||||
case StateConfigConfirmation:
|
||||
return m.viewConfigConfirmation()
|
||||
case StateDeployingConfigs:
|
||||
return m.viewDeployingConfigs()
|
||||
case StateInstallComplete:
|
||||
return m.viewInstallComplete()
|
||||
case StateError:
|
||||
return m.viewError()
|
||||
default:
|
||||
return m.viewWelcome()
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) listenForLogs() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
select {
|
||||
case msg, ok := <-m.logChan:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return logMsg{message: msg}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) detectOS() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
info, err := distros.GetOSInfo()
|
||||
osInfoMsg := &distros.OSInfo{}
|
||||
if info != nil {
|
||||
osInfoMsg.Distribution = info.Distribution
|
||||
osInfoMsg.Version = info.Version
|
||||
osInfoMsg.VersionID = info.VersionID
|
||||
osInfoMsg.PrettyName = info.PrettyName
|
||||
osInfoMsg.Architecture = info.Architecture
|
||||
}
|
||||
return osInfoCompleteMsg{info: osInfoMsg, err: err}
|
||||
}
|
||||
}
|
||||
21
core/internal/tui/banner.go
Normal file
21
core/internal/tui/banner.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package tui
|
||||
|
||||
import "github.com/charmbracelet/lipgloss"
|
||||
|
||||
func (m Model) renderBanner() string {
|
||||
logo := `
|
||||
██████╗ █████╗ ███╗ ██╗██╗ ██╗
|
||||
██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝
|
||||
██║ ██║███████║██╔██╗ ██║█████╔╝
|
||||
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
|
||||
██████╔╝██║ ██║██║ ╚████║██║ ██╗
|
||||
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ `
|
||||
|
||||
theme := TerminalTheme()
|
||||
style := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Primary)).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
return style.Render(logo)
|
||||
}
|
||||
39
core/internal/tui/messages.go
Normal file
39
core/internal/tui/messages.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
)
|
||||
|
||||
type logMsg struct {
|
||||
message string
|
||||
}
|
||||
|
||||
type osInfoCompleteMsg struct {
|
||||
info *distros.OSInfo
|
||||
err error
|
||||
}
|
||||
|
||||
type depsDetectedMsg struct {
|
||||
deps []deps.Dependency
|
||||
err error
|
||||
}
|
||||
|
||||
type packageInstallProgressMsg struct {
|
||||
progress float64
|
||||
step string
|
||||
isComplete bool
|
||||
needsSudo bool
|
||||
commandInfo string
|
||||
logOutput string
|
||||
error error
|
||||
}
|
||||
|
||||
type packageProgressCompletedMsg struct{}
|
||||
|
||||
type passwordValidMsg struct {
|
||||
password string
|
||||
valid bool
|
||||
}
|
||||
|
||||
type delayCompleteMsg struct{}
|
||||
23
core/internal/tui/states.go
Normal file
23
core/internal/tui/states.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package tui
|
||||
|
||||
type ApplicationState int
|
||||
|
||||
const (
|
||||
StateWelcome ApplicationState = iota
|
||||
StateSelectWindowManager
|
||||
StateSelectTerminal
|
||||
StateMissingWMInstructions
|
||||
StateDetectingDeps
|
||||
StateDependencyReview
|
||||
StateGentooUseFlags
|
||||
StateGentooGCCCheck
|
||||
StateAuthMethodChoice
|
||||
StateFingerprintAuth
|
||||
StatePasswordPrompt
|
||||
StateInstallingPackages
|
||||
StateConfigConfirmation
|
||||
StateDeployingConfigs
|
||||
StateInstallComplete
|
||||
StateFinalComplete
|
||||
StateError
|
||||
)
|
||||
124
core/internal/tui/styles.go
Normal file
124
core/internal/tui/styles.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/progress"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type AppTheme struct {
|
||||
Primary string
|
||||
Secondary string
|
||||
Accent string
|
||||
Text string
|
||||
Subtle string
|
||||
Error string
|
||||
Warning string
|
||||
Success string
|
||||
Background string
|
||||
Surface string
|
||||
}
|
||||
|
||||
func TerminalTheme() AppTheme {
|
||||
return AppTheme{
|
||||
Primary: "6", // #625690 - purple
|
||||
Secondary: "5", // #36247a - dark purple
|
||||
Accent: "12", // #7060ac - light purple
|
||||
Text: "7", // #2e2e2e - dark gray
|
||||
Subtle: "8", // #4a4a4a - medium gray
|
||||
Error: "1", // #d83636 - red
|
||||
Warning: "3", // #ffff89 - yellow
|
||||
Success: "2", // #53e550 - green
|
||||
Background: "15", // #1a1a1a - near black
|
||||
Surface: "8", // #4a4a4a - medium gray
|
||||
}
|
||||
}
|
||||
|
||||
func NewStyles(theme AppTheme) Styles {
|
||||
return Styles{
|
||||
Title: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Primary)).
|
||||
Bold(true).
|
||||
MarginLeft(1).
|
||||
MarginBottom(1),
|
||||
|
||||
Normal: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Text)),
|
||||
|
||||
Bold: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Text)).
|
||||
Bold(true),
|
||||
|
||||
Subtle: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Subtle)),
|
||||
|
||||
Error: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Error)),
|
||||
|
||||
Warning: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Warning)),
|
||||
|
||||
StatusBar: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#33275e")).
|
||||
Background(lipgloss.Color(theme.Primary)).
|
||||
Padding(0, 1),
|
||||
|
||||
Key: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Accent)).
|
||||
Bold(true),
|
||||
|
||||
SpinnerStyle: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Primary)),
|
||||
|
||||
Success: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Success)).
|
||||
Bold(true),
|
||||
|
||||
HighlightButton: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#33275e")).
|
||||
Background(lipgloss.Color(theme.Primary)).
|
||||
Padding(0, 2).
|
||||
Bold(true),
|
||||
|
||||
SelectedOption: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Accent)).
|
||||
Bold(true),
|
||||
|
||||
CodeBlock: lipgloss.NewStyle().
|
||||
Background(lipgloss.Color(theme.Surface)).
|
||||
Foreground(lipgloss.Color(theme.Text)).
|
||||
Padding(1, 2).
|
||||
MarginLeft(2),
|
||||
}
|
||||
}
|
||||
|
||||
type Styles struct {
|
||||
Title lipgloss.Style
|
||||
Normal lipgloss.Style
|
||||
Bold lipgloss.Style
|
||||
Subtle lipgloss.Style
|
||||
Warning lipgloss.Style
|
||||
Error lipgloss.Style
|
||||
StatusBar lipgloss.Style
|
||||
Key lipgloss.Style
|
||||
SpinnerStyle lipgloss.Style
|
||||
Success lipgloss.Style
|
||||
HighlightButton lipgloss.Style
|
||||
SelectedOption lipgloss.Style
|
||||
CodeBlock lipgloss.Style
|
||||
}
|
||||
|
||||
func (s Styles) NewThemedProgress(width int) progress.Model {
|
||||
theme := TerminalTheme()
|
||||
prog := progress.New(
|
||||
progress.WithGradient(theme.Secondary, theme.Primary),
|
||||
)
|
||||
|
||||
prog.Width = width
|
||||
prog.ShowPercentage = true
|
||||
prog.PercentFormat = "%.0f%%"
|
||||
prog.PercentageStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Text)).
|
||||
Bold(true)
|
||||
|
||||
return prog
|
||||
}
|
||||
382
core/internal/tui/views_config.go
Normal file
382
core/internal/tui/views_config.go
Normal file
@@ -0,0 +1,382 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type configDeploymentResult struct {
|
||||
results []config.DeploymentResult
|
||||
error error
|
||||
}
|
||||
|
||||
type ExistingConfigInfo struct {
|
||||
ConfigType string
|
||||
Path string
|
||||
Exists bool
|
||||
}
|
||||
|
||||
type configCheckResult struct {
|
||||
configs []ExistingConfigInfo
|
||||
error error
|
||||
}
|
||||
|
||||
func (m Model) viewDeployingConfigs() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
title := m.styles.Title.Render("Deploying Configurations")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
spinner := m.spinner.View()
|
||||
status := m.styles.Normal.Render("Setting up configuration files...")
|
||||
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Show progress information
|
||||
info := m.styles.Subtle.Render("• Creating backups of existing configurations\n• Deploying optimized configurations\n• Detecting system paths")
|
||||
b.WriteString(info)
|
||||
|
||||
// Show live log output if available
|
||||
if len(m.installationLogs) > 0 {
|
||||
b.WriteString("\n\n")
|
||||
logHeader := m.styles.Subtle.Render("Configuration Log:")
|
||||
b.WriteString(logHeader)
|
||||
b.WriteString("\n")
|
||||
|
||||
// Show last few lines of logs
|
||||
maxLines := 5
|
||||
startIdx := 0
|
||||
if len(m.installationLogs) > maxLines {
|
||||
startIdx = len(m.installationLogs) - maxLines
|
||||
}
|
||||
|
||||
for i := startIdx; i < len(m.installationLogs); i++ {
|
||||
if m.installationLogs[i] != "" {
|
||||
logLine := m.styles.Subtle.Render(" " + m.installationLogs[i])
|
||||
b.WriteString(logLine)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) updateDeployingConfigsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if result, ok := msg.(configDeploymentResult); ok {
|
||||
if result.error != nil {
|
||||
m.err = result.error
|
||||
m.state = StateError
|
||||
m.isLoading = false
|
||||
return m, nil
|
||||
}
|
||||
|
||||
for _, deployResult := range result.results {
|
||||
if deployResult.Deployed {
|
||||
logMsg := fmt.Sprintf("✓ %s configuration deployed", deployResult.ConfigType)
|
||||
if deployResult.BackupPath != "" {
|
||||
logMsg += fmt.Sprintf(" (backup: %s)", deployResult.BackupPath)
|
||||
}
|
||||
m.installationLogs = append(m.installationLogs, logMsg)
|
||||
}
|
||||
}
|
||||
|
||||
m.state = StateInstallComplete
|
||||
m.isLoading = false
|
||||
return m, nil
|
||||
}
|
||||
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
|
||||
func (m Model) deployConfigurations() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
// Determine the selected window manager
|
||||
var wm deps.WindowManager
|
||||
switch m.selectedWM {
|
||||
case 0:
|
||||
wm = deps.WindowManagerNiri
|
||||
case 1:
|
||||
wm = deps.WindowManagerHyprland
|
||||
default:
|
||||
wm = deps.WindowManagerNiri
|
||||
}
|
||||
|
||||
// Determine the selected terminal
|
||||
var terminal deps.Terminal
|
||||
if m.osInfo != nil && m.osInfo.Distribution.ID == "gentoo" {
|
||||
switch m.selectedTerminal {
|
||||
case 0:
|
||||
terminal = deps.TerminalKitty
|
||||
case 1:
|
||||
terminal = deps.TerminalAlacritty
|
||||
default:
|
||||
terminal = deps.TerminalKitty
|
||||
}
|
||||
} else {
|
||||
switch m.selectedTerminal {
|
||||
case 0:
|
||||
terminal = deps.TerminalGhostty
|
||||
case 1:
|
||||
terminal = deps.TerminalKitty
|
||||
default:
|
||||
terminal = deps.TerminalAlacritty
|
||||
}
|
||||
}
|
||||
|
||||
deployer := config.NewConfigDeployer(m.logChan)
|
||||
|
||||
results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(context.Background(), wm, terminal, m.dependencies, m.replaceConfigs, m.reinstallItems)
|
||||
|
||||
return configDeploymentResult{
|
||||
results: results,
|
||||
error: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) viewConfigConfirmation() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
title := m.styles.Title.Render("Configuration Deployment")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(m.existingConfigs) == 0 {
|
||||
// No existing configs, proceed directly
|
||||
info := m.styles.Normal.Render("No existing configurations found. Proceeding with deployment...")
|
||||
b.WriteString(info)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Show existing configurations with toggle options
|
||||
for i, configInfo := range m.existingConfigs {
|
||||
if configInfo.Exists {
|
||||
var status string
|
||||
var replaceMarker string
|
||||
|
||||
shouldReplace := m.replaceConfigs[configInfo.ConfigType]
|
||||
if _, exists := m.replaceConfigs[configInfo.ConfigType]; !exists {
|
||||
shouldReplace = true
|
||||
m.replaceConfigs[configInfo.ConfigType] = true
|
||||
}
|
||||
|
||||
if shouldReplace {
|
||||
replaceMarker = "🔄 "
|
||||
status = m.styles.Warning.Render("Will replace")
|
||||
} else {
|
||||
replaceMarker = "✓ "
|
||||
status = m.styles.Success.Render("Keep existing")
|
||||
}
|
||||
|
||||
var line string
|
||||
if i == m.selectedConfig {
|
||||
line = fmt.Sprintf("▶ %s%-15s %s", replaceMarker, configInfo.ConfigType, status)
|
||||
line += fmt.Sprintf("\n %s", configInfo.Path)
|
||||
line = m.styles.SelectedOption.Render(line)
|
||||
} else {
|
||||
line = fmt.Sprintf(" %s%-15s %s", replaceMarker, configInfo.ConfigType, status)
|
||||
line += fmt.Sprintf("\n %s", configInfo.Path)
|
||||
line = m.styles.Normal.Render(line)
|
||||
}
|
||||
|
||||
b.WriteString(line)
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
backup := m.styles.Success.Render("✓ Replaced configurations will be backed up with timestamp")
|
||||
b.WriteString(backup)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
help := m.styles.Subtle.Render("↑/↓: Navigate, Space: Toggle replace/keep, Enter: Continue")
|
||||
b.WriteString(help)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) updateConfigConfirmationState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if result, ok := msg.(configCheckResult); ok {
|
||||
if result.error != nil {
|
||||
m.err = result.error
|
||||
m.state = StateError
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.existingConfigs = result.configs
|
||||
|
||||
firstExistingSet := false
|
||||
for i, config := range result.configs {
|
||||
if config.Exists {
|
||||
m.replaceConfigs[config.ConfigType] = true
|
||||
if !firstExistingSet {
|
||||
m.selectedConfig = i
|
||||
firstExistingSet = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasExisting := false
|
||||
for _, config := range result.configs {
|
||||
if config.Exists {
|
||||
hasExisting = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasExisting {
|
||||
// No existing configs, proceed directly to deployment
|
||||
m.state = StateDeployingConfigs
|
||||
return m, m.deployConfigurations()
|
||||
}
|
||||
|
||||
// Show confirmation view
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
switch keyMsg.String() {
|
||||
case "up":
|
||||
if m.selectedConfig > 0 {
|
||||
for i := m.selectedConfig - 1; i >= 0; i-- {
|
||||
if m.existingConfigs[i].Exists {
|
||||
m.selectedConfig = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case "down":
|
||||
if m.selectedConfig < len(m.existingConfigs)-1 {
|
||||
for i := m.selectedConfig + 1; i < len(m.existingConfigs); i++ {
|
||||
if m.existingConfigs[i].Exists {
|
||||
m.selectedConfig = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case " ":
|
||||
if len(m.existingConfigs) > 0 && m.selectedConfig < len(m.existingConfigs) {
|
||||
configType := m.existingConfigs[m.selectedConfig].ConfigType
|
||||
if m.existingConfigs[m.selectedConfig].Exists {
|
||||
m.replaceConfigs[configType] = !m.replaceConfigs[configType]
|
||||
}
|
||||
}
|
||||
case "enter":
|
||||
m.state = StateDeployingConfigs
|
||||
return m, m.deployConfigurations()
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) checkExistingConfigurations() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
var configs []ExistingConfigInfo
|
||||
|
||||
if m.selectedWM == 0 {
|
||||
niriPath := filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl")
|
||||
niriExists := false
|
||||
if _, err := os.Stat(niriPath); err == nil {
|
||||
niriExists = true
|
||||
}
|
||||
configs = append(configs, ExistingConfigInfo{
|
||||
ConfigType: "Niri",
|
||||
Path: niriPath,
|
||||
Exists: niriExists,
|
||||
})
|
||||
} else {
|
||||
hyprlandPath := filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf")
|
||||
hyprlandExists := false
|
||||
if _, err := os.Stat(hyprlandPath); err == nil {
|
||||
hyprlandExists = true
|
||||
}
|
||||
configs = append(configs, ExistingConfigInfo{
|
||||
ConfigType: "Hyprland",
|
||||
Path: hyprlandPath,
|
||||
Exists: hyprlandExists,
|
||||
})
|
||||
}
|
||||
|
||||
if m.osInfo != nil && m.osInfo.Distribution.ID == "gentoo" {
|
||||
if m.selectedTerminal == 0 {
|
||||
kittyPath := filepath.Join(os.Getenv("HOME"), ".config", "kitty", "kitty.conf")
|
||||
kittyExists := false
|
||||
if _, err := os.Stat(kittyPath); err == nil {
|
||||
kittyExists = true
|
||||
}
|
||||
configs = append(configs, ExistingConfigInfo{
|
||||
ConfigType: "Kitty",
|
||||
Path: kittyPath,
|
||||
Exists: kittyExists,
|
||||
})
|
||||
} else {
|
||||
alacrittyPath := filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "alacritty.toml")
|
||||
alacrittyExists := false
|
||||
if _, err := os.Stat(alacrittyPath); err == nil {
|
||||
alacrittyExists = true
|
||||
}
|
||||
configs = append(configs, ExistingConfigInfo{
|
||||
ConfigType: "Alacritty",
|
||||
Path: alacrittyPath,
|
||||
Exists: alacrittyExists,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
switch m.selectedTerminal {
|
||||
case 0:
|
||||
ghosttyPath := filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config")
|
||||
ghosttyExists := false
|
||||
if _, err := os.Stat(ghosttyPath); err == nil {
|
||||
ghosttyExists = true
|
||||
}
|
||||
configs = append(configs, ExistingConfigInfo{
|
||||
ConfigType: "Ghostty",
|
||||
Path: ghosttyPath,
|
||||
Exists: ghosttyExists,
|
||||
})
|
||||
case 1:
|
||||
kittyPath := filepath.Join(os.Getenv("HOME"), ".config", "kitty", "kitty.conf")
|
||||
kittyExists := false
|
||||
if _, err := os.Stat(kittyPath); err == nil {
|
||||
kittyExists = true
|
||||
}
|
||||
configs = append(configs, ExistingConfigInfo{
|
||||
ConfigType: "Kitty",
|
||||
Path: kittyPath,
|
||||
Exists: kittyExists,
|
||||
})
|
||||
default:
|
||||
alacrittyPath := filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "alacritty.toml")
|
||||
alacrittyExists := false
|
||||
if _, err := os.Stat(alacrittyPath); err == nil {
|
||||
alacrittyExists = true
|
||||
}
|
||||
configs = append(configs, ExistingConfigInfo{
|
||||
ConfigType: "Alacritty",
|
||||
Path: alacrittyPath,
|
||||
Exists: alacrittyExists,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return configCheckResult{
|
||||
configs: configs,
|
||||
error: nil,
|
||||
}
|
||||
}
|
||||
}
|
||||
256
core/internal/tui/views_dependencies.go
Normal file
256
core/internal/tui/views_dependencies.go
Normal file
@@ -0,0 +1,256 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) viewDetectingDeps() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
title := m.styles.Title.Render("Detecting Dependencies")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
spinner := m.spinner.View()
|
||||
status := m.styles.Normal.Render("Scanning system for existing packages and configurations...")
|
||||
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) viewDependencyReview() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
title := m.styles.Title.Render("Dependency Review")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(m.dependencies) > 0 {
|
||||
for i, dep := range m.dependencies {
|
||||
var status string
|
||||
var reinstallMarker string
|
||||
var variantMarker string
|
||||
|
||||
isDMS := dep.Name == "dms (DankMaterialShell)"
|
||||
|
||||
if dep.CanToggle && dep.Variant == deps.VariantGit {
|
||||
variantMarker = "[git] "
|
||||
}
|
||||
|
||||
if m.disabledItems[dep.Name] {
|
||||
reinstallMarker = "✗ "
|
||||
status = m.styles.Subtle.Render("Will skip")
|
||||
} else if m.reinstallItems[dep.Name] {
|
||||
reinstallMarker = "🔄 "
|
||||
status = m.styles.Warning.Render("Will upgrade")
|
||||
} else if isDMS {
|
||||
reinstallMarker = "⚡ "
|
||||
switch dep.Status {
|
||||
case deps.StatusInstalled:
|
||||
status = m.styles.Success.Render("✓ Required (installed)")
|
||||
case deps.StatusMissing:
|
||||
status = m.styles.Warning.Render("○ Required (will install)")
|
||||
case deps.StatusNeedsUpdate:
|
||||
status = m.styles.Warning.Render("△ Required (needs update)")
|
||||
case deps.StatusNeedsReinstall:
|
||||
status = m.styles.Error.Render("! Required (needs reinstall)")
|
||||
}
|
||||
} else {
|
||||
switch dep.Status {
|
||||
case deps.StatusInstalled:
|
||||
status = m.styles.Subtle.Render("✓ Already installed")
|
||||
case deps.StatusMissing:
|
||||
status = m.styles.Warning.Render("○ Will install")
|
||||
case deps.StatusNeedsUpdate:
|
||||
status = m.styles.Warning.Render("△ Will install")
|
||||
case deps.StatusNeedsReinstall:
|
||||
status = m.styles.Error.Render("! Will install")
|
||||
}
|
||||
}
|
||||
|
||||
var line string
|
||||
if i == m.selectedDep {
|
||||
line = fmt.Sprintf("▶ %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
|
||||
if dep.Version != "" {
|
||||
line += fmt.Sprintf(" (%s)", dep.Version)
|
||||
}
|
||||
line = m.styles.SelectedOption.Render(line)
|
||||
} else {
|
||||
line = fmt.Sprintf(" %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
|
||||
if dep.Version != "" {
|
||||
line += fmt.Sprintf(" (%s)", dep.Version)
|
||||
}
|
||||
line = m.styles.Normal.Render(line)
|
||||
}
|
||||
|
||||
b.WriteString(line)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
help := m.styles.Subtle.Render("↑/↓: Navigate, Space: Toggle, G: Toggle stable/git, Enter: Continue")
|
||||
b.WriteString(help)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) updateDetectingDepsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if depsMsg, ok := msg.(depsDetectedMsg); ok {
|
||||
m.isLoading = false
|
||||
if depsMsg.err != nil {
|
||||
m.err = depsMsg.err
|
||||
m.state = StateError
|
||||
} else {
|
||||
m.dependencies = depsMsg.deps
|
||||
m.state = StateDependencyReview
|
||||
}
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
|
||||
func (m Model) updateDependencyReviewState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
switch keyMsg.String() {
|
||||
case "up":
|
||||
if m.selectedDep > 0 {
|
||||
m.selectedDep--
|
||||
}
|
||||
case "down":
|
||||
if m.selectedDep < len(m.dependencies)-1 {
|
||||
m.selectedDep++
|
||||
}
|
||||
case " ":
|
||||
if len(m.dependencies) > 0 {
|
||||
depName := m.dependencies[m.selectedDep].Name
|
||||
isDMS := depName == "dms (DankMaterialShell)"
|
||||
|
||||
if !isDMS {
|
||||
isInstalled := m.dependencies[m.selectedDep].Status == deps.StatusInstalled ||
|
||||
m.dependencies[m.selectedDep].Status == deps.StatusNeedsReinstall
|
||||
|
||||
if isInstalled {
|
||||
m.reinstallItems[depName] = !m.reinstallItems[depName]
|
||||
m.disabledItems[depName] = false
|
||||
} else {
|
||||
m.disabledItems[depName] = !m.disabledItems[depName]
|
||||
m.reinstallItems[depName] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
case "g", "G":
|
||||
if len(m.dependencies) > 0 && m.dependencies[m.selectedDep].CanToggle {
|
||||
if m.dependencies[m.selectedDep].Variant == deps.VariantStable {
|
||||
m.dependencies[m.selectedDep].Variant = deps.VariantGit
|
||||
} else {
|
||||
m.dependencies[m.selectedDep].Variant = deps.VariantStable
|
||||
}
|
||||
}
|
||||
case "enter":
|
||||
// Check if on Gentoo - show USE flags screen
|
||||
if m.osInfo != nil {
|
||||
if config, exists := distros.Registry[m.osInfo.Distribution.ID]; exists && config.Family == distros.FamilyGentoo {
|
||||
m.state = StateGentooUseFlags
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
// Check if fingerprint is enabled
|
||||
if checkFingerprintEnabled() {
|
||||
m.state = StateAuthMethodChoice
|
||||
m.selectedConfig = 0 // Default to fingerprint
|
||||
return m, nil
|
||||
} else {
|
||||
m.state = StatePasswordPrompt
|
||||
m.passwordInput.Focus()
|
||||
return m, nil
|
||||
}
|
||||
case "esc":
|
||||
m.state = StateSelectWindowManager
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
|
||||
func (m Model) installPackages() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if m.osInfo == nil {
|
||||
return packageInstallProgressMsg{
|
||||
progress: 0.0,
|
||||
step: "Error: OS info not available",
|
||||
isComplete: true,
|
||||
}
|
||||
}
|
||||
|
||||
installer, err := distros.NewPackageInstaller(m.osInfo.Distribution.ID, m.logChan)
|
||||
if err != nil {
|
||||
return packageInstallProgressMsg{
|
||||
progress: 0.0,
|
||||
step: fmt.Sprintf("Error: %s", err.Error()),
|
||||
isComplete: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert TUI selection to deps enum
|
||||
var wm deps.WindowManager
|
||||
if m.selectedWM == 0 {
|
||||
wm = deps.WindowManagerNiri
|
||||
} else {
|
||||
wm = deps.WindowManagerHyprland
|
||||
}
|
||||
|
||||
installerProgressChan := make(chan distros.InstallProgressMsg, 100)
|
||||
|
||||
go func() {
|
||||
defer close(installerProgressChan)
|
||||
err := installer.InstallPackages(context.Background(), m.dependencies, wm, m.sudoPassword, m.reinstallItems, m.disabledItems, m.skipGentooUseFlags, installerProgressChan)
|
||||
if err != nil {
|
||||
installerProgressChan <- distros.InstallProgressMsg{
|
||||
Progress: 0.0,
|
||||
Step: fmt.Sprintf("Installation error: %s", err.Error()),
|
||||
IsComplete: true,
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Convert installer messages to TUI messages
|
||||
go func() {
|
||||
for msg := range installerProgressChan {
|
||||
tuiMsg := packageInstallProgressMsg{
|
||||
progress: msg.Progress,
|
||||
step: msg.Step,
|
||||
isComplete: msg.IsComplete,
|
||||
needsSudo: msg.NeedsSudo,
|
||||
commandInfo: msg.CommandInfo,
|
||||
logOutput: msg.LogOutput,
|
||||
error: msg.Error,
|
||||
}
|
||||
if msg.IsComplete {
|
||||
m.logChan <- fmt.Sprintf("[DEBUG] Sending completion signal: step=%s, progress=%.2f", msg.Step, msg.Progress)
|
||||
}
|
||||
m.packageProgressChan <- tuiMsg
|
||||
}
|
||||
m.logChan <- "[DEBUG] Installer channel closed"
|
||||
}()
|
||||
|
||||
return packageInstallProgressMsg{
|
||||
progress: 0.05,
|
||||
step: "Starting installation...",
|
||||
isComplete: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
103
core/internal/tui/views_gentoo_gcc.go
Normal file
103
core/internal/tui/views_gentoo_gcc.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type gccVersionCheckMsg struct {
|
||||
version string
|
||||
major int
|
||||
err error
|
||||
}
|
||||
|
||||
func (m Model) viewGentooGCCCheck() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
title := m.styles.Title.Render("GCC Version Check Failed")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
error := m.styles.Error.Render("⚠ Hyprland requires GCC 15 or newer")
|
||||
b.WriteString(error)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
info := m.styles.Normal.Render("Your current GCC version is too old. Please upgrade GCC before continuing.")
|
||||
b.WriteString(info)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionsTitle := m.styles.Subtle.Render("To upgrade GCC:")
|
||||
b.WriteString(instructionsTitle)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
steps := []string{
|
||||
"1. Install GCC 15 (if not already installed):",
|
||||
" sudo emerge -1av =sys-devel/gcc:15",
|
||||
"",
|
||||
"2. Switch to GCC 15 using gcc-config:",
|
||||
" sudo gcc-config $(gcc-config -l | grep gcc-15 | awk '{print $2}' | head -1)",
|
||||
"",
|
||||
"3. Update environment:",
|
||||
" source /etc/profile",
|
||||
"",
|
||||
"4. Restart this installer",
|
||||
}
|
||||
|
||||
for _, step := range steps {
|
||||
stepLine := m.styles.Subtle.Render(step)
|
||||
b.WriteString(stepLine)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
help := m.styles.Subtle.Render("Press Esc to go back, Ctrl+C to exit")
|
||||
b.WriteString(help)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) updateGentooGCCCheckState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
switch keyMsg.String() {
|
||||
case "esc":
|
||||
m.state = StateSelectWindowManager
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
|
||||
func (m Model) checkGCCVersion() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
cmd := exec.CommandContext(context.Background(), "gcc", "-dumpversion")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return gccVersionCheckMsg{err: err}
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(string(output))
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) == 0 {
|
||||
return gccVersionCheckMsg{err: fmt.Errorf("invalid gcc version format")}
|
||||
}
|
||||
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return gccVersionCheckMsg{err: err}
|
||||
}
|
||||
|
||||
return gccVersionCheckMsg{
|
||||
version: version,
|
||||
major: major,
|
||||
err: nil,
|
||||
}
|
||||
}
|
||||
}
|
||||
92
core/internal/tui/views_gentoo_use_flags.go
Normal file
92
core/internal/tui/views_gentoo_use_flags.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) viewGentooUseFlags() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
title := m.styles.Title.Render("Gentoo Global USE Flags")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
info := m.styles.Normal.Render("The following global USE flags will be enabled in /etc/portage/make.conf:")
|
||||
b.WriteString(info)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
for _, flag := range distros.GentooGlobalUseFlags {
|
||||
flagLine := m.styles.Success.Render(fmt.Sprintf(" • %s", flag))
|
||||
b.WriteString(flagLine)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
note := m.styles.Subtle.Render("These flags ensure proper Qt6, Wayland, and compositor support.")
|
||||
b.WriteString(note)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
var toggleLine string
|
||||
if m.skipGentooUseFlags {
|
||||
toggleLine = "▶ [✗] Skip adding global USE flags (will use existing configuration)"
|
||||
toggleLine = m.styles.Warning.Render(toggleLine)
|
||||
} else {
|
||||
toggleLine = " [ ] Skip adding global USE flags (will use existing configuration)"
|
||||
toggleLine = m.styles.Subtle.Render(toggleLine)
|
||||
}
|
||||
b.WriteString(toggleLine)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
help := m.styles.Subtle.Render("Space: Toggle skip, Enter: Continue, Esc: Go back")
|
||||
b.WriteString(help)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) updateGentooUseFlagsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if gccMsg, ok := msg.(gccVersionCheckMsg); ok {
|
||||
if gccMsg.err != nil || gccMsg.major < 15 {
|
||||
m.state = StateGentooGCCCheck
|
||||
return m, nil
|
||||
}
|
||||
if checkFingerprintEnabled() {
|
||||
m.state = StateAuthMethodChoice
|
||||
m.selectedConfig = 0
|
||||
} else {
|
||||
m.state = StatePasswordPrompt
|
||||
m.passwordInput.Focus()
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
switch keyMsg.String() {
|
||||
case " ":
|
||||
m.skipGentooUseFlags = !m.skipGentooUseFlags
|
||||
return m, nil
|
||||
case "enter":
|
||||
if m.selectedWM == 1 {
|
||||
return m, m.checkGCCVersion()
|
||||
}
|
||||
if checkFingerprintEnabled() {
|
||||
m.state = StateAuthMethodChoice
|
||||
m.selectedConfig = 0
|
||||
} else {
|
||||
m.state = StatePasswordPrompt
|
||||
m.passwordInput.Focus()
|
||||
}
|
||||
return m, nil
|
||||
case "esc":
|
||||
m.state = StateDependencyReview
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
333
core/internal/tui/views_install.go
Normal file
333
core/internal/tui/views_install.go
Normal file
@@ -0,0 +1,333 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// wrapText wraps text to the specified width
|
||||
func wrapText(text string, width int) string {
|
||||
if len(text) <= width {
|
||||
return text
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
words := strings.Fields(text)
|
||||
currentLine := ""
|
||||
|
||||
for _, word := range words {
|
||||
if len(currentLine) == 0 {
|
||||
currentLine = word
|
||||
} else if len(currentLine)+1+len(word) <= width {
|
||||
currentLine += " " + word
|
||||
} else {
|
||||
result.WriteString(currentLine)
|
||||
result.WriteString("\n")
|
||||
currentLine = word
|
||||
}
|
||||
}
|
||||
|
||||
if len(currentLine) > 0 {
|
||||
result.WriteString(currentLine)
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func (m Model) viewInstallingPackages() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
title := m.styles.Title.Render("Installing Packages")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if !m.packageProgress.isComplete {
|
||||
spinner := m.spinner.View()
|
||||
status := m.styles.Normal.Render(m.packageProgress.step)
|
||||
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Show progress bar
|
||||
progressBar := fmt.Sprintf("[%s%s] %.0f%%",
|
||||
strings.Repeat("█", int(m.packageProgress.progress*30)),
|
||||
strings.Repeat("░", 30-int(m.packageProgress.progress*30)),
|
||||
m.packageProgress.progress*100)
|
||||
b.WriteString(m.styles.Normal.Render(progressBar))
|
||||
b.WriteString("\n")
|
||||
|
||||
// Show command info if available
|
||||
if m.packageProgress.commandInfo != "" {
|
||||
cmdInfo := m.styles.Subtle.Render("$ " + m.packageProgress.commandInfo)
|
||||
b.WriteString(cmdInfo)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
// Show live log output
|
||||
if len(m.installationLogs) > 0 {
|
||||
b.WriteString("\n")
|
||||
logHeader := m.styles.Subtle.Render("Live Output:")
|
||||
b.WriteString(logHeader)
|
||||
b.WriteString("\n")
|
||||
|
||||
// Show last few lines of accumulated logs
|
||||
maxLines := 8
|
||||
startIdx := 0
|
||||
if len(m.installationLogs) > maxLines {
|
||||
startIdx = len(m.installationLogs) - maxLines
|
||||
}
|
||||
|
||||
for i := startIdx; i < len(m.installationLogs); i++ {
|
||||
if m.installationLogs[i] != "" {
|
||||
logLine := m.styles.Subtle.Render(" " + m.installationLogs[i])
|
||||
b.WriteString(logLine)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show error if any
|
||||
if m.packageProgress.error != nil {
|
||||
b.WriteString("\n")
|
||||
wrappedErrorMsg := wrapText("Error: "+m.packageProgress.error.Error(), 80)
|
||||
errorMsg := m.styles.Error.Render(wrappedErrorMsg)
|
||||
b.WriteString(errorMsg)
|
||||
}
|
||||
|
||||
// Show sudo prompt if needed
|
||||
if m.packageProgress.needsSudo {
|
||||
sudoWarning := m.styles.Warning.Render("⚠ Using provided sudo password")
|
||||
b.WriteString(sudoWarning)
|
||||
}
|
||||
} else {
|
||||
if m.packageProgress.error != nil {
|
||||
wrappedFailedMsg := wrapText("✗ Installation failed: "+m.packageProgress.error.Error(), 80)
|
||||
errorMsg := m.styles.Error.Render(wrappedFailedMsg)
|
||||
b.WriteString(errorMsg)
|
||||
} else {
|
||||
success := m.styles.Success.Render("✓ Installation complete!")
|
||||
b.WriteString(success)
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) viewInstallComplete() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
title := m.styles.Success.Render("Setup Complete!")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
success := m.styles.Success.Render("✓ All packages installed and configurations deployed.")
|
||||
b.WriteString(success)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Show what was accomplished
|
||||
accomplishments := []string{
|
||||
"• Window manager and dependencies installed",
|
||||
"• Terminal and development tools configured",
|
||||
"• Configuration files deployed with backups",
|
||||
"• System optimized for DankMaterialShell",
|
||||
}
|
||||
|
||||
for _, item := range accomplishments {
|
||||
b.WriteString(m.styles.Subtle.Render(item))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
info := m.styles.Normal.Render("Your system is ready! Log out and log back in to start using\nyour new desktop environment.\nIf you do not have a greeter, login with \"niri-session\" or \"Hyprland\" \n\nPress Enter to exit.")
|
||||
b.WriteString(info)
|
||||
|
||||
if m.logFilePath != "" {
|
||||
b.WriteString("\n\n")
|
||||
logInfo := m.styles.Subtle.Render(fmt.Sprintf("Full logs: %s", m.logFilePath))
|
||||
b.WriteString(logInfo)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) viewError() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
title := m.styles.Error.Render("Installation Failed")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if m.err != nil {
|
||||
wrappedError := wrapText("✗ "+m.err.Error(), 80)
|
||||
error := m.styles.Error.Render(wrappedError)
|
||||
b.WriteString(error)
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// Show package progress error if available
|
||||
if m.packageProgress.error != nil {
|
||||
wrappedPackageError := wrapText("Package Installation Error: "+m.packageProgress.error.Error(), 80)
|
||||
packageError := m.styles.Error.Render(wrappedPackageError)
|
||||
b.WriteString(packageError)
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// Show persistent installation logs
|
||||
if len(m.installationLogs) > 0 {
|
||||
logHeader := m.styles.Warning.Render("Installation Logs (last 15 lines):")
|
||||
b.WriteString(logHeader)
|
||||
b.WriteString("\n")
|
||||
|
||||
maxLines := 15
|
||||
startIdx := 0
|
||||
if len(m.installationLogs) > maxLines {
|
||||
startIdx = len(m.installationLogs) - maxLines
|
||||
}
|
||||
|
||||
for i := startIdx; i < len(m.installationLogs); i++ {
|
||||
if m.installationLogs[i] != "" {
|
||||
logLine := m.styles.Subtle.Render(" " + m.installationLogs[i])
|
||||
b.WriteString(logLine)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
hint := m.styles.Subtle.Render("Press Ctrl+D for full debug logs")
|
||||
b.WriteString(hint)
|
||||
b.WriteString("\n")
|
||||
|
||||
if m.logFilePath != "" {
|
||||
b.WriteString("\n")
|
||||
logInfo := m.styles.Warning.Render(fmt.Sprintf("Full logs: %s", m.logFilePath))
|
||||
b.WriteString(logInfo)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
help := m.styles.Subtle.Render("Press Enter to exit")
|
||||
b.WriteString(help)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) updateInstallingPackagesState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if progressMsg, ok := msg.(packageInstallProgressMsg); ok {
|
||||
m.packageProgress = progressMsg
|
||||
|
||||
// Accumulate log output
|
||||
if progressMsg.logOutput != "" {
|
||||
m.installationLogs = append(m.installationLogs, progressMsg.logOutput)
|
||||
// Keep only last 50 lines to preserve more context for debugging
|
||||
if len(m.installationLogs) > 50 {
|
||||
m.installationLogs = m.installationLogs[len(m.installationLogs)-50:]
|
||||
}
|
||||
}
|
||||
|
||||
if progressMsg.isComplete {
|
||||
if progressMsg.error != nil {
|
||||
m.state = StateError
|
||||
m.isLoading = false
|
||||
} else {
|
||||
m.installationLogs = []string{}
|
||||
m.state = StateConfigConfirmation
|
||||
m.isLoading = true
|
||||
return m, tea.Batch(m.spinner.Tick, m.checkExistingConfigurations())
|
||||
}
|
||||
}
|
||||
return m, m.listenForPackageProgress()
|
||||
}
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
|
||||
func (m Model) updateInstallCompleteState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
switch keyMsg.String() {
|
||||
case "enter":
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
|
||||
func (m Model) updateErrorState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
switch keyMsg.String() {
|
||||
case "enter":
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
|
||||
func (m Model) listenForPackageProgress() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
msg, ok := <-m.packageProgressChan
|
||||
if !ok {
|
||||
return packageProgressCompletedMsg{}
|
||||
}
|
||||
// Always return the message, completion will be handled in updateInstallingPackagesState
|
||||
return msg
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) viewDebugLogs() string {
|
||||
var b strings.Builder
|
||||
|
||||
theme := TerminalTheme()
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Primary)).
|
||||
Bold(true)
|
||||
|
||||
b.WriteString(titleStyle.Render("Debug Logs"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Combine both logMessages and installationLogs
|
||||
allLogs := append([]string{}, m.logMessages...)
|
||||
allLogs = append(allLogs, m.installationLogs...)
|
||||
|
||||
if len(allLogs) == 0 {
|
||||
b.WriteString("No logs available\n")
|
||||
} else {
|
||||
// Calculate available height (reserve space for header and footer)
|
||||
maxHeight := m.height - 6
|
||||
if maxHeight < 10 {
|
||||
maxHeight = 10
|
||||
}
|
||||
|
||||
// Show the most recent logs
|
||||
startIdx := 0
|
||||
if len(allLogs) > maxHeight {
|
||||
startIdx = len(allLogs) - maxHeight
|
||||
}
|
||||
|
||||
for i := startIdx; i < len(allLogs); i++ {
|
||||
if allLogs[i] != "" {
|
||||
b.WriteString(fmt.Sprintf("%d: %s\n", i, allLogs[i]))
|
||||
}
|
||||
}
|
||||
|
||||
if startIdx > 0 {
|
||||
subtleStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle))
|
||||
b.WriteString(subtleStyle.Render(fmt.Sprintf("... (%d older log entries hidden)\n", startIdx)))
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Accent))
|
||||
b.WriteString(statusStyle.Render("Press Ctrl+D to return, Ctrl+C to quit"))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
85
core/internal/tui/views_nixos_wm.go
Normal file
85
core/internal/tui/views_nixos_wm.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) viewMissingWMInstructions() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Determine which WM is missing
|
||||
wmName := "Niri"
|
||||
installCmd := `environment.systemPackages = with pkgs; [
|
||||
niri
|
||||
];`
|
||||
alternateCmd := `# Or enable the module if available:
|
||||
# programs.niri.enable = true;`
|
||||
|
||||
if m.selectedWM == 1 {
|
||||
wmName = "Hyprland"
|
||||
installCmd = `programs.hyprland.enable = true;`
|
||||
alternateCmd = `# Or add to systemPackages:
|
||||
# environment.systemPackages = with pkgs; [
|
||||
# hyprland
|
||||
# ];`
|
||||
}
|
||||
|
||||
// Title
|
||||
title := m.styles.Title.Render("⚠️ " + wmName + " Not Installed")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Explanation
|
||||
explanation := m.styles.Normal.Render(wmName + " needs to be installed system-wide on NixOS.")
|
||||
b.WriteString(explanation)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Instructions
|
||||
instructions := m.styles.Subtle.Render("To install " + wmName + ", add this to your /etc/nixos/configuration.nix:")
|
||||
b.WriteString(instructions)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Command box
|
||||
cmdBox := m.styles.CodeBlock.Render(installCmd)
|
||||
b.WriteString(cmdBox)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Alternate command
|
||||
altBox := m.styles.Subtle.Render(alternateCmd)
|
||||
b.WriteString(altBox)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Rebuild instruction
|
||||
rebuildInstruction := m.styles.Normal.Render("Then rebuild your system:")
|
||||
b.WriteString(rebuildInstruction)
|
||||
b.WriteString("\n")
|
||||
|
||||
rebuildCmd := m.styles.CodeBlock.Render("sudo nixos-rebuild switch")
|
||||
b.WriteString(rebuildCmd)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Navigation help
|
||||
help := m.styles.Subtle.Render("Press Esc to go back and select a different window manager, or Ctrl+C to exit")
|
||||
b.WriteString(help)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) updateMissingWMInstructionsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
switch keyMsg.String() {
|
||||
case "esc":
|
||||
// Go back to window manager selection
|
||||
m.state = StateSelectWindowManager
|
||||
return m, m.listenForLogs()
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
338
core/internal/tui/views_password.go
Normal file
338
core/internal/tui/views_password.go
Normal file
@@ -0,0 +1,338 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) viewAuthMethodChoice() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
title := m.styles.Title.Render("Authentication Method")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
message := "Fingerprint authentication is available.\nHow would you like to authenticate?"
|
||||
b.WriteString(m.styles.Normal.Render(message))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Option 0: Fingerprint
|
||||
if m.selectedConfig == 0 {
|
||||
option := m.styles.SelectedOption.Render("▶ Use Fingerprint")
|
||||
b.WriteString(option)
|
||||
} else {
|
||||
option := m.styles.Normal.Render(" Use Fingerprint")
|
||||
b.WriteString(option)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
// Option 1: Password
|
||||
if m.selectedConfig == 1 {
|
||||
option := m.styles.SelectedOption.Render("▶ Use Password")
|
||||
b.WriteString(option)
|
||||
} else {
|
||||
option := m.styles.Normal.Render(" Use Password")
|
||||
b.WriteString(option)
|
||||
}
|
||||
b.WriteString("\n\n")
|
||||
|
||||
help := m.styles.Subtle.Render("↑/↓: Navigate, Enter: Select, Esc: Back")
|
||||
b.WriteString(help)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) viewFingerprintAuth() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
title := m.styles.Title.Render("Fingerprint Authentication")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if m.fingerprintFailed {
|
||||
errorMsg := m.styles.Error.Render("✗ Fingerprint authentication failed")
|
||||
b.WriteString(errorMsg)
|
||||
b.WriteString("\n")
|
||||
retryMsg := m.styles.Subtle.Render("Returning to authentication menu...")
|
||||
b.WriteString(retryMsg)
|
||||
} else {
|
||||
message := "Please place your finger on the fingerprint reader."
|
||||
b.WriteString(m.styles.Normal.Render(message))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
spinner := m.spinner.View()
|
||||
status := m.styles.Normal.Render("Waiting for fingerprint...")
|
||||
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) viewPasswordPrompt() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
title := m.styles.Title.Render("Password Authentication")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
message := "Installation requires sudo privileges.\nPlease enter your password to continue:"
|
||||
b.WriteString(m.styles.Normal.Render(message))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Password input
|
||||
b.WriteString(m.passwordInput.View())
|
||||
b.WriteString("\n")
|
||||
|
||||
// Show validation status
|
||||
if m.packageProgress.step == "Validating sudo password..." {
|
||||
spinner := m.spinner.View()
|
||||
status := m.styles.Normal.Render(m.packageProgress.step)
|
||||
b.WriteString(spinner + " " + status)
|
||||
b.WriteString("\n")
|
||||
} else if m.packageProgress.error != nil {
|
||||
errorMsg := m.styles.Error.Render("✗ " + m.packageProgress.error.Error() + ". Please try again.")
|
||||
b.WriteString(errorMsg)
|
||||
b.WriteString("\n")
|
||||
} else if m.packageProgress.step == "Password validation failed" {
|
||||
errorMsg := m.styles.Error.Render("✗ Incorrect password. Please try again.")
|
||||
b.WriteString(errorMsg)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
help := m.styles.Subtle.Render("Enter: Continue, Esc: Back, Ctrl+C: Cancel")
|
||||
b.WriteString(help)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) updateAuthMethodChoiceState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.fingerprintFailed = false
|
||||
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
switch keyMsg.String() {
|
||||
case "up":
|
||||
if m.selectedConfig > 0 {
|
||||
m.selectedConfig--
|
||||
}
|
||||
case "down":
|
||||
if m.selectedConfig < 1 {
|
||||
m.selectedConfig++
|
||||
}
|
||||
case "enter":
|
||||
if m.selectedConfig == 0 {
|
||||
m.state = StateFingerprintAuth
|
||||
m.isLoading = true
|
||||
return m, tea.Batch(m.spinner.Tick, m.tryFingerprint())
|
||||
} else {
|
||||
m.state = StatePasswordPrompt
|
||||
m.passwordInput.Focus()
|
||||
return m, nil
|
||||
}
|
||||
case "esc":
|
||||
m.state = StateDependencyReview
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateFingerprintAuthState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if validMsg, ok := msg.(passwordValidMsg); ok {
|
||||
if validMsg.valid {
|
||||
m.sudoPassword = ""
|
||||
m.packageProgress = packageInstallProgressMsg{}
|
||||
m.state = StateInstallingPackages
|
||||
m.isLoading = true
|
||||
return m, tea.Batch(m.spinner.Tick, m.installPackages())
|
||||
} else {
|
||||
m.fingerprintFailed = true
|
||||
return m, m.delayThenReturn()
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := msg.(delayCompleteMsg); ok {
|
||||
m.fingerprintFailed = false
|
||||
m.selectedConfig = 0
|
||||
m.state = StateAuthMethodChoice
|
||||
return m, nil
|
||||
}
|
||||
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
|
||||
func (m Model) updatePasswordPromptState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
|
||||
if validMsg, ok := msg.(passwordValidMsg); ok {
|
||||
if validMsg.valid {
|
||||
// Password is valid, proceed with installation
|
||||
m.sudoPassword = validMsg.password
|
||||
m.passwordInput.SetValue("") // Clear password input
|
||||
// Clear any error state
|
||||
m.packageProgress = packageInstallProgressMsg{}
|
||||
m.state = StateInstallingPackages
|
||||
m.isLoading = true
|
||||
return m, tea.Batch(m.spinner.Tick, m.installPackages())
|
||||
} else {
|
||||
// Password is invalid, show error and stay on password prompt
|
||||
m.packageProgress = packageInstallProgressMsg{
|
||||
progress: 0.0,
|
||||
step: "Password validation failed",
|
||||
error: fmt.Errorf("incorrect password"),
|
||||
logOutput: "Authentication failed",
|
||||
}
|
||||
m.passwordInput.SetValue("")
|
||||
m.passwordInput.Focus()
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
switch keyMsg.String() {
|
||||
case "enter":
|
||||
// Don't allow multiple validation attempts while one is in progress
|
||||
if m.packageProgress.step == "Validating sudo password..." {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Validate password first
|
||||
password := m.passwordInput.Value()
|
||||
if password == "" {
|
||||
return m, nil // Don't proceed with empty password
|
||||
}
|
||||
|
||||
// Clear any previous error and show validation in progress
|
||||
m.packageProgress = packageInstallProgressMsg{
|
||||
progress: 0.01,
|
||||
step: "Validating sudo password...",
|
||||
isComplete: false,
|
||||
logOutput: "Testing password with sudo -v",
|
||||
}
|
||||
return m, m.validatePassword(password)
|
||||
case "esc":
|
||||
// Go back to dependency review
|
||||
m.passwordInput.SetValue("")
|
||||
m.packageProgress = packageInstallProgressMsg{} // Clear any validation state
|
||||
m.state = StateDependencyReview
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
m.passwordInput, cmd = m.passwordInput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func checkFingerprintEnabled() bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Check if pam_fprintd.so is in PAM config
|
||||
cmd := exec.CommandContext(ctx, "grep", "-q", "pam_fprintd.so", "/etc/pam.d/system-auth")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if fprintd-list exists and user has enrolled fingerprints
|
||||
user := os.Getenv("USER")
|
||||
if user == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
listCmd := exec.CommandContext(ctx, "fprintd-list", user)
|
||||
output, err := listCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// If output contains "finger:" or similar, fingerprints are enrolled
|
||||
return strings.Contains(string(output), "finger")
|
||||
}
|
||||
|
||||
func (m Model) delayThenReturn() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
time.Sleep(2 * time.Second)
|
||||
return delayCompleteMsg{}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) tryFingerprint() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
clearCmd := exec.Command("sudo", "-k")
|
||||
clearCmd.Run()
|
||||
|
||||
tmpDir := os.TempDir()
|
||||
askpassScript := filepath.Join(tmpDir, fmt.Sprintf("danklinux-fp-%d.sh", time.Now().UnixNano()))
|
||||
|
||||
scriptContent := "#!/bin/sh\nexit 1\n"
|
||||
if err := os.WriteFile(askpassScript, []byte(scriptContent), 0700); err != nil {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
defer os.Remove(askpassScript)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sudo", "-A", "-v")
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("SUDO_ASKPASS=%s", askpassScript))
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
if err != nil {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
return passwordValidMsg{password: "", valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) validatePassword(password string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(stdin, "%s\n", password)
|
||||
stdin.Close()
|
||||
if err != nil {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
|
||||
if err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
return passwordValidMsg{password: password, valid: true}
|
||||
}
|
||||
}
|
||||
244
core/internal/tui/views_selection.go
Normal file
244
core/internal/tui/views_selection.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) viewSelectWindowManager() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
title := m.styles.Title.Render("Choose Window Manager")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
options := []struct {
|
||||
name string
|
||||
description string
|
||||
}{
|
||||
{"niri", "Scrollable-tiling Wayland compositor."},
|
||||
}
|
||||
|
||||
if m.osInfo == nil || m.osInfo.Distribution.ID != "debian" {
|
||||
options = append(options, struct {
|
||||
name string
|
||||
description string
|
||||
}{"Hyprland", "Dynamic tiling Wayland compositor."})
|
||||
}
|
||||
|
||||
for i, option := range options {
|
||||
if i == m.selectedWM {
|
||||
selected := m.styles.SelectedOption.Render("▶ " + option.name)
|
||||
b.WriteString(selected)
|
||||
b.WriteString("\n")
|
||||
desc := m.styles.Subtle.Render(" " + option.description)
|
||||
b.WriteString(desc)
|
||||
} else {
|
||||
normal := m.styles.Normal.Render(" " + option.name)
|
||||
b.WriteString(normal)
|
||||
b.WriteString("\n")
|
||||
desc := m.styles.Subtle.Render(" " + option.description)
|
||||
b.WriteString(desc)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
if i < len(options)-1 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
help := m.styles.Subtle.Render("Use ↑/↓ to navigate, Enter to select, Esc to go back")
|
||||
b.WriteString(help)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) viewSelectTerminal() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
title := m.styles.Title.Render("Choose Terminal Emulator")
|
||||
b.WriteString(title)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
options := []struct {
|
||||
name string
|
||||
description string
|
||||
}{}
|
||||
|
||||
if m.osInfo != nil && m.osInfo.Distribution.ID == "gentoo" {
|
||||
options = []struct {
|
||||
name string
|
||||
description string
|
||||
}{
|
||||
{"kitty", "A feature-rich, customizable terminal emulator."},
|
||||
{"alacritty", "A simple terminal emulator."},
|
||||
}
|
||||
} else {
|
||||
options = []struct {
|
||||
name string
|
||||
description string
|
||||
}{
|
||||
{"ghostty", "A fast, native terminal emulator built in Zig."},
|
||||
{"kitty", "A feature-rich, customizable terminal emulator."},
|
||||
{"alacritty", "A simple terminal emulator."},
|
||||
}
|
||||
}
|
||||
|
||||
for i, option := range options {
|
||||
if i == m.selectedTerminal {
|
||||
selected := m.styles.SelectedOption.Render("▶ " + option.name)
|
||||
b.WriteString(selected)
|
||||
b.WriteString("\n")
|
||||
desc := m.styles.Subtle.Render(" " + option.description)
|
||||
b.WriteString(desc)
|
||||
} else {
|
||||
normal := m.styles.Normal.Render(" " + option.name)
|
||||
b.WriteString(normal)
|
||||
b.WriteString("\n")
|
||||
desc := m.styles.Subtle.Render(" " + option.description)
|
||||
b.WriteString(desc)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
if i < len(options)-1 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
help := m.styles.Subtle.Render("Use ↑/↓ to navigate, Enter to select, Esc to go back")
|
||||
b.WriteString(help)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) updateSelectTerminalState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
maxTerminalIndex := 2
|
||||
if m.osInfo != nil && m.osInfo.Distribution.ID == "gentoo" {
|
||||
maxTerminalIndex = 1
|
||||
}
|
||||
|
||||
switch keyMsg.String() {
|
||||
case "up":
|
||||
if m.selectedTerminal > 0 {
|
||||
m.selectedTerminal--
|
||||
}
|
||||
case "down":
|
||||
if m.selectedTerminal < maxTerminalIndex {
|
||||
m.selectedTerminal++
|
||||
}
|
||||
case "enter":
|
||||
if m.osInfo != nil && m.osInfo.Distribution.ID == "nixos" {
|
||||
var wmInstalled bool
|
||||
if m.selectedWM == 0 {
|
||||
wmInstalled = m.commandExists("niri")
|
||||
} else {
|
||||
wmInstalled = m.commandExists("hyprland") || m.commandExists("Hyprland")
|
||||
}
|
||||
|
||||
if !wmInstalled {
|
||||
m.state = StateMissingWMInstructions
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
}
|
||||
|
||||
m.state = StateDetectingDeps
|
||||
m.isLoading = true
|
||||
return m, tea.Batch(m.spinner.Tick, m.detectDependencies())
|
||||
case "esc":
|
||||
m.state = StateSelectWindowManager
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
}
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
|
||||
func (m Model) updateSelectWindowManagerState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
maxWMIndex := 1
|
||||
if m.osInfo != nil && m.osInfo.Distribution.ID == "debian" {
|
||||
maxWMIndex = 0
|
||||
}
|
||||
|
||||
switch keyMsg.String() {
|
||||
case "up":
|
||||
if m.selectedWM > 0 {
|
||||
m.selectedWM--
|
||||
}
|
||||
case "down":
|
||||
if m.selectedWM < maxWMIndex {
|
||||
m.selectedWM++
|
||||
}
|
||||
case "enter":
|
||||
m.state = StateSelectTerminal
|
||||
return m, m.listenForLogs()
|
||||
case "esc":
|
||||
// Go back to welcome screen
|
||||
m.state = StateWelcome
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
}
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
|
||||
func (m Model) commandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (m Model) detectDependencies() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if m.osInfo == nil {
|
||||
return depsDetectedMsg{deps: nil, err: fmt.Errorf("OS info not available")}
|
||||
}
|
||||
|
||||
detector, err := distros.NewDependencyDetector(m.osInfo.Distribution.ID, m.logChan)
|
||||
if err != nil {
|
||||
return depsDetectedMsg{deps: nil, err: err}
|
||||
}
|
||||
|
||||
// Convert TUI selection to deps enum
|
||||
var wm deps.WindowManager
|
||||
if m.selectedWM == 0 {
|
||||
wm = deps.WindowManagerNiri // First option is Niri
|
||||
} else {
|
||||
wm = deps.WindowManagerHyprland // Second option is Hyprland
|
||||
}
|
||||
|
||||
var terminal deps.Terminal
|
||||
if m.osInfo != nil && m.osInfo.Distribution.ID == "gentoo" {
|
||||
switch m.selectedTerminal {
|
||||
case 0:
|
||||
terminal = deps.TerminalKitty
|
||||
case 1:
|
||||
terminal = deps.TerminalAlacritty
|
||||
default:
|
||||
terminal = deps.TerminalKitty
|
||||
}
|
||||
} else {
|
||||
switch m.selectedTerminal {
|
||||
case 0:
|
||||
terminal = deps.TerminalGhostty
|
||||
case 1:
|
||||
terminal = deps.TerminalKitty
|
||||
default:
|
||||
terminal = deps.TerminalAlacritty
|
||||
}
|
||||
}
|
||||
|
||||
dependencies, err := detector.DetectDependenciesWithTerminal(context.Background(), wm, terminal)
|
||||
return depsDetectedMsg{deps: dependencies, err: err}
|
||||
}
|
||||
}
|
||||
216
core/internal/tui/views_welcome.go
Normal file
216
core/internal/tui/views_welcome.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) viewWelcome() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
theme := TerminalTheme()
|
||||
|
||||
decorator := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Accent)).
|
||||
Render("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
|
||||
titleBox := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color(theme.Primary)).
|
||||
Padding(0, 2).
|
||||
MarginBottom(1)
|
||||
|
||||
titleText := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Primary)).
|
||||
Bold(true).
|
||||
Render("dankinstall")
|
||||
|
||||
versionTag := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Accent)).
|
||||
Italic(true).
|
||||
Render(" // Dank Linux Installer")
|
||||
|
||||
subtitle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Subtle)).
|
||||
Italic(true).
|
||||
Render("Quickstart for a Dank™ Desktop")
|
||||
|
||||
b.WriteString(decorator)
|
||||
b.WriteString("\n")
|
||||
b.WriteString(titleBox.Render(titleText + versionTag))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(subtitle)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if m.osInfo != nil {
|
||||
if distros.IsUnsupportedDistro(m.osInfo.Distribution.ID, m.osInfo.VersionID) {
|
||||
errorBox := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("#FF6B6B")).
|
||||
Padding(1, 2).
|
||||
MarginBottom(1)
|
||||
|
||||
errorTitle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF6B6B")).
|
||||
Bold(true).
|
||||
Render("⚠ UNSUPPORTED DISTRIBUTION")
|
||||
|
||||
var errorMsg string
|
||||
switch m.osInfo.Distribution.ID {
|
||||
case "ubuntu":
|
||||
errorMsg = fmt.Sprintf("Ubuntu %s is not supported.\n\nOnly Ubuntu 25.04+ is supported.\n\nPlease upgrade to Ubuntu 25.04 or later.", m.osInfo.VersionID)
|
||||
case "debian":
|
||||
errorMsg = fmt.Sprintf("Debian %s is not supported.\n\nOnly Debian 13+ (Trixie) is supported.\n\nPlease upgrade to Debian 13 or later.", m.osInfo.VersionID)
|
||||
case "nixos":
|
||||
errorMsg = "NixOS is currently not supported, but there is a DankMaterialShell flake available."
|
||||
default:
|
||||
errorMsg = fmt.Sprintf("%s is not supported.\nFeel free to request on https://github.com/AvengeMedia/DankMaterialShell", m.osInfo.PrettyName)
|
||||
}
|
||||
|
||||
errorMsgStyled := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Text)).
|
||||
Render(errorMsg)
|
||||
|
||||
b.WriteString(errorBox.Render(errorTitle + "\n\n" + errorMsgStyled))
|
||||
b.WriteString("\n\n")
|
||||
} else {
|
||||
// System info box
|
||||
sysBox := lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color(theme.Subtle)).
|
||||
Padding(0, 1).
|
||||
MarginBottom(1)
|
||||
|
||||
// Style the distro name with its color
|
||||
distroStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(m.osInfo.Distribution.HexColorCode)).
|
||||
Bold(true)
|
||||
distroName := distroStyle.Render(m.osInfo.PrettyName)
|
||||
|
||||
archStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Accent))
|
||||
|
||||
sysInfo := fmt.Sprintf("System: %s / %s", distroName, archStyle.Render(m.osInfo.Architecture))
|
||||
b.WriteString(sysBox.Render(sysInfo))
|
||||
b.WriteString("\n")
|
||||
|
||||
// Feature list with better styling
|
||||
featTitle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Primary)).
|
||||
Bold(true).
|
||||
Underline(true).
|
||||
Render("WHAT YOU GET")
|
||||
b.WriteString(featTitle + "\n\n")
|
||||
|
||||
features := []string{
|
||||
"[shell] dms (DankMaterialShell)",
|
||||
"[wm] niri or Hyprland",
|
||||
"[term] Ghostty, kitty, or Alacritty",
|
||||
"[style] All the themes, automatically.",
|
||||
"[config] DANK defaults - keybindings, rules, animations, etc.",
|
||||
}
|
||||
|
||||
for i, feat := range features {
|
||||
prefix := feat[:9]
|
||||
content := feat[10:]
|
||||
|
||||
prefixStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Accent)).
|
||||
Bold(true)
|
||||
|
||||
contentStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Text))
|
||||
|
||||
if i == len(features)-1 {
|
||||
contentStyle = contentStyle.Bold(true)
|
||||
}
|
||||
|
||||
b.WriteString(fmt.Sprintf(" %s %s\n",
|
||||
prefixStyle.Render(prefix),
|
||||
contentStyle.Render(content)))
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
|
||||
noteStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Subtle)).
|
||||
Italic(true)
|
||||
note := noteStyle.Render("* Existing configs can be replaced (and backed up) or preserved")
|
||||
b.WriteString(note)
|
||||
b.WriteString("\n")
|
||||
|
||||
if m.osInfo.Distribution.ID == "gentoo" {
|
||||
gentooNote := noteStyle.Render("* Will set per-package USE flags and unmask testing packages as needed")
|
||||
b.WriteString(gentooNote)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
} else if m.isLoading {
|
||||
spinner := m.spinner.View()
|
||||
loading := m.styles.Normal.Render("Detecting system...")
|
||||
b.WriteString(fmt.Sprintf("%s %s\n\n", spinner, loading))
|
||||
}
|
||||
|
||||
// Footer with better visual separation
|
||||
footerDivider := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Subtle)).
|
||||
Render("───────────────────────────────────────────────────────────")
|
||||
b.WriteString(footerDivider + "\n")
|
||||
|
||||
if m.osInfo != nil {
|
||||
ctrlKey := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Primary)).
|
||||
Bold(true).
|
||||
Render("Ctrl+C")
|
||||
|
||||
if distros.IsUnsupportedDistro(m.osInfo.Distribution.ID, m.osInfo.VersionID) {
|
||||
b.WriteString(m.styles.Subtle.Render("Press ") + ctrlKey + m.styles.Subtle.Render(" to quit"))
|
||||
} else {
|
||||
enterKey := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Primary)).
|
||||
Bold(true).
|
||||
Render("Enter")
|
||||
|
||||
b.WriteString(m.styles.Subtle.Render("Press ") + enterKey + m.styles.Subtle.Render(" to choose window manager, ") + ctrlKey + m.styles.Subtle.Render(" to quit"))
|
||||
}
|
||||
} else {
|
||||
help := m.styles.Subtle.Render("Press Enter to continue, Ctrl+C to quit")
|
||||
b.WriteString(help)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) updateWelcomeState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if completeMsg, ok := msg.(osInfoCompleteMsg); ok {
|
||||
m.isLoading = false
|
||||
if completeMsg.err != nil {
|
||||
m.err = completeMsg.err
|
||||
m.state = StateError
|
||||
} else {
|
||||
m.osInfo = completeMsg.info
|
||||
}
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
switch keyMsg.String() {
|
||||
case "enter":
|
||||
if m.osInfo != nil && !distros.IsUnsupportedDistro(m.osInfo.Distribution.ID, m.osInfo.VersionID) {
|
||||
m.state = StateSelectWindowManager
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, m.listenForLogs()
|
||||
}
|
||||
Reference in New Issue
Block a user