diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui')
13 files changed, 2510 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/app.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/app.go new file mode 100644 index 0000000..d07430a --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/app.go @@ -0,0 +1,240 @@ +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 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 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} + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/banner.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/banner.go new file mode 100644 index 0000000..e3a2ca3 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/banner.go @@ -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) +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/messages.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/messages.go new file mode 100644 index 0000000..7c03098 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/messages.go @@ -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{} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/states.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/states.go new file mode 100644 index 0000000..6e9e194 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/states.go @@ -0,0 +1,22 @@ +package tui + +type ApplicationState int + +const ( + StateWelcome ApplicationState = iota + StateSelectWindowManager + StateSelectTerminal + StateDetectingDeps + StateDependencyReview + StateGentooUseFlags + StateGentooGCCCheck + StateAuthMethodChoice + StateFingerprintAuth + StatePasswordPrompt + StateInstallingPackages + StateConfigConfirmation + StateDeployingConfigs + StateInstallComplete + StateFinalComplete + StateError +) diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/styles.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/styles.go new file mode 100644 index 0000000..45a9dae --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/styles.go @@ -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 +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_config.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_config.go new file mode 100644 index 0000000..14daff3 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_config.go @@ -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...") + fmt.Fprintf(&b, "%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, + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_dependencies.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_dependencies.go new file mode 100644 index 0000000..13f0335 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_dependencies.go @@ -0,0 +1,304 @@ +package tui + +import ( + "context" + "fmt" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" + "github.com/AvengeMedia/DankMaterialShell/core/internal/distros" + "github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" + 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...") + fmt.Fprintf(&b, "%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") + } + } + + note := "" + if dep.Name == "dms-greeter" { + note = m.styles.Subtle.Render(" (selection replaces your current display manager)") + } + + 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) + note + } 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) + note + } + + 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 + // dms-greeter is opt-in skipped by default + for _, dep := range depsMsg.deps { + if dep.Name == "dms-greeter" { + m.disabledItems["dms-greeter"] = true + break + } + } + 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 { + // Run optional greeter setup + if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil { + greeterSelected := false + for _, dep := range m.dependencies { + if dep.Name == "dms-greeter" && !m.disabledItems["dms-greeter"] { + greeterSelected = true + break + } + } + if greeterSelected { + compositorName := "niri" + if m.selectedWM == 1 { + compositorName = "Hyprland" + } + m.packageProgressChan <- packageInstallProgressMsg{ + progress: 0.92, + step: "Configuring DMS greeter...", + logOutput: "Starting automated greeter setup...", + } + greeterLogFunc := func(line string) { + m.packageProgressChan <- packageInstallProgressMsg{ + progress: 0.94, + step: "Configuring DMS greeter...", + logOutput: line, + } + } + if err := greeter.AutoSetupGreeter(compositorName, m.sudoPassword, greeterLogFunc); err != nil { + m.packageProgressChan <- packageInstallProgressMsg{ + progress: 0.96, + step: "Greeter setup warning", + logOutput: fmt.Sprintf("⚠ Greeter auto-setup warning (non-fatal): %v", err), + } + } + } + } + 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, + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_gentoo_gcc.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_gentoo_gcc.go new file mode 100644 index 0000000..74195a4 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_gentoo_gcc.go @@ -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, + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_gentoo_use_flags.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_gentoo_use_flags.go new file mode 100644 index 0000000..eb259ef --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_gentoo_use_flags.go @@ -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() +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_install.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_install.go new file mode 100644 index 0000000..c110711 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_install.go @@ -0,0 +1,405 @@ +package tui + +import ( + "fmt" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" + "github.com/AvengeMedia/DankMaterialShell/core/internal/distros" + 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) + fmt.Fprintf(&b, "%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 dmsPackageName(distroID string, dependencies []deps.Dependency) string { + config, ok := distros.Registry[distroID] + if !ok { + return "dms" + } + + var isGit bool + for _, dep := range dependencies { + if dep.Name == "dms (DankMaterialShell)" { + isGit = dep.Variant == deps.VariantGit + break + } + } + + switch config.Family { + case distros.FamilyArch: + if isGit { + return "dms-shell-git" + } + return "dms-shell" + case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE: + if isGit { + return "dms-git" + } + return "dms" + default: + return "dms" + } +} + +func uninstallCommand(distroID string, dependencies []deps.Dependency) string { + config, ok := distros.Registry[distroID] + if !ok { + return "" + } + if config.Family == distros.FamilyGentoo { + return "rm -rf ~/.config/quickshell/dms && sudo rm /usr/local/bin/dms" + } + pkg := dmsPackageName(distroID, dependencies) + switch config.Family { + case distros.FamilyArch: + return "sudo pacman -Rs " + pkg + case distros.FamilyFedora: + return "sudo dnf remove " + pkg + case distros.FamilyUbuntu, distros.FamilyDebian: + return "sudo apt remove " + pkg + case distros.FamilySUSE: + return "sudo zypper remove " + pkg + default: + return "" + } +} + +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") + + 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\"") + b.WriteString(info) + b.WriteString("\n\n") + + theme := TerminalTheme() + cmdStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Accent)) + labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle)) + + b.WriteString(labelStyle.Render("Troubleshooting:") + "\n") + b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("systemctl --user disable dms") + "\n") + b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("journalctl --user -u dms") + "\n") + + if m.osInfo != nil { + if cmd := uninstallCommand(m.osInfo.Distribution.ID, m.dependencies); cmd != "" { + b.WriteString(labelStyle.Render(" Uninstall: ") + cmdStyle.Render(cmd) + "\n") + } + } + + b.WriteString("\n") + b.WriteString(m.styles.Normal.Render("Press Enter to exit.")) + + 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] != "" { + fmt.Fprintf(&b, "%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() +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_password.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_password.go new file mode 100644 index 0000000..deec061 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_password.go @@ -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...") + fmt.Fprintf(&b, "%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), 0o700); 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} + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_selection.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_selection.go new file mode 100644 index 0000000..e45105e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_selection.go @@ -0,0 +1,224 @@ +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) 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") + + var 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": + 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) 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} + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_welcome.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_welcome.go new file mode 100644 index 0000000..c5ae862 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_welcome.go @@ -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 = "See the NixOS documentation for installation instructions: https://danklinux.com/docs/dankmaterialshell/nixos." + 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) + } + + fmt.Fprintf(&b, " %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...") + fmt.Fprintf(&b, "%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() +} |