summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui
diff options
context:
space:
mode:
authorAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
committerAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
commit70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch)
treeab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui
parent5d94c0a7d44a2255b81815a52a7056a94a39842d (diff)
downloadRaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz
RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/app.go240
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/banner.go21
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/messages.go39
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/states.go22
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/styles.go124
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_config.go382
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_dependencies.go304
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_gentoo_gcc.go103
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_gentoo_use_flags.go92
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_install.go405
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_password.go338
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_selection.go224
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/tui/views_welcome.go216
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()
+}