mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-30 06:57:49 -04:00
377 lines
10 KiB
Go
377 lines
10 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
func wrapText(text string, width int) string {
|
|
if len(text) <= width {
|
|
return text
|
|
}
|
|
|
|
var result strings.Builder
|
|
currentLine := ""
|
|
for word := range strings.FieldsSeq(text) {
|
|
switch {
|
|
case currentLine == "":
|
|
currentLine = word
|
|
case len(currentLine)+1+len(word) <= width:
|
|
currentLine += " " + word
|
|
default:
|
|
result.WriteString(currentLine)
|
|
result.WriteByte('\n')
|
|
currentLine = word
|
|
}
|
|
}
|
|
result.WriteString(currentLine)
|
|
return result.String()
|
|
}
|
|
|
|
func tailLines(lines []string, n int) []string {
|
|
if len(lines) <= n {
|
|
return lines
|
|
}
|
|
return lines[len(lines)-n:]
|
|
}
|
|
|
|
func (m Model) writeRecentLogs(b *strings.Builder, renderedHeader string, maxLines int) {
|
|
if len(m.installationLogs) == 0 {
|
|
return
|
|
}
|
|
|
|
b.WriteString(renderedHeader)
|
|
b.WriteByte('\n')
|
|
for _, line := range tailLines(m.installationLogs, maxLines) {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
b.WriteString(m.styles.Subtle.Render(" " + line))
|
|
b.WriteByte('\n')
|
|
}
|
|
}
|
|
|
|
func (m Model) viewInstallingPackages() string {
|
|
var b strings.Builder
|
|
|
|
b.WriteString(m.renderBanner())
|
|
b.WriteByte('\n')
|
|
b.WriteString(m.styles.Title.Render("Installing Packages"))
|
|
b.WriteString("\n\n")
|
|
|
|
if m.packageProgress.isComplete {
|
|
if m.packageProgress.error != nil {
|
|
b.WriteString(m.styles.Error.Render(wrapText("✗ Installation failed: "+m.packageProgress.error.Error(), 80)))
|
|
return b.String()
|
|
}
|
|
b.WriteString(m.styles.Success.Render("✓ Installation complete!"))
|
|
return b.String()
|
|
}
|
|
|
|
fmt.Fprintf(&b, "%s %s\n\n", m.spinner.View(), m.styles.Normal.Render(m.packageProgress.step))
|
|
b.WriteString(m.styles.Normal.Render(m.renderInstallProgressBar()))
|
|
b.WriteByte('\n')
|
|
|
|
if m.packageProgress.commandInfo != "" {
|
|
b.WriteString(m.styles.Subtle.Render("$ " + m.packageProgress.commandInfo))
|
|
b.WriteByte('\n')
|
|
}
|
|
|
|
if len(m.installationLogs) > 0 {
|
|
b.WriteByte('\n')
|
|
m.writeRecentLogs(&b, m.styles.Subtle.Render("Live Output:"), 8)
|
|
}
|
|
|
|
if m.packageProgress.error != nil {
|
|
b.WriteByte('\n')
|
|
b.WriteString(m.styles.Error.Render(wrapText("Error: "+m.packageProgress.error.Error(), 80)))
|
|
}
|
|
|
|
if m.packageProgress.needsSudo {
|
|
b.WriteString(m.styles.Warning.Render("⚠ Using provided sudo password"))
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func (m Model) renderInstallProgressBar() string {
|
|
const barWidth = 30
|
|
filled := min(max(int(m.packageProgress.progress*barWidth), 0), barWidth)
|
|
return fmt.Sprintf("[%s%s] %.0f%%",
|
|
strings.Repeat("█", filled), strings.Repeat("░", barWidth-filled), m.packageProgress.progress*100)
|
|
}
|
|
|
|
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 == dmsDepName {
|
|
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, distros.FamilyVoid:
|
|
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 "sudo emerge --deselect gui-apps/dankmaterialshell && sudo emerge --depclean gui-apps/dankmaterialshell"
|
|
}
|
|
|
|
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
|
|
case distros.FamilyVoid:
|
|
return "sudo xbps-remove -R " + pkg
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (m Model) loginHint() string {
|
|
wm := m.chosenWindowManager()
|
|
|
|
if !m.useSystemdConfig() {
|
|
switch wm {
|
|
case deps.WindowManagerNiri:
|
|
return "If you do not have a greeter, from a TTY run: dbus-run-session niri"
|
|
case deps.WindowManagerHyprland:
|
|
return "If you do not have a greeter, from a TTY run: dbus-run-session Hyprland"
|
|
default:
|
|
return "If you do not have a greeter, from a TTY run: dbus-run-session mango"
|
|
}
|
|
}
|
|
|
|
switch wm {
|
|
case deps.WindowManagerNiri:
|
|
return `If you do not have a greeter, login with "niri-session"`
|
|
case deps.WindowManagerHyprland:
|
|
return `If you do not have a greeter, login with "Hyprland"`
|
|
default:
|
|
return `If you do not have a greeter, login with "mango"`
|
|
}
|
|
}
|
|
|
|
func (m Model) troubleshootingHints() (autostart, logs string) {
|
|
wm := m.chosenWindowManager()
|
|
|
|
if !m.useSystemdConfig() {
|
|
logs = "quickshell --path ~/.config/quickshell/dms log"
|
|
switch wm {
|
|
case deps.WindowManagerNiri:
|
|
autostart = `remove spawn-at-startup "dms" "run" from ~/.config/niri/config.kdl`
|
|
case deps.WindowManagerHyprland:
|
|
autostart = `remove hl.exec_cmd("dms run") from ~/.config/hypr/hyprland.lua`
|
|
default:
|
|
autostart = "remove 'exec-once=dms run' from ~/.config/mango/config.conf"
|
|
}
|
|
return autostart, logs
|
|
}
|
|
|
|
if wm == deps.WindowManagerMango {
|
|
return "remove 'exec-once=dms run' from ~/.config/mango/config.conf", "qs -p ~/.config/quickshell/dms log"
|
|
}
|
|
return "systemctl --user disable dms", "journalctl --user -u dms"
|
|
}
|
|
|
|
func (m Model) viewInstallComplete() string {
|
|
var b strings.Builder
|
|
|
|
b.WriteString(m.renderBanner())
|
|
b.WriteByte('\n')
|
|
b.WriteString(m.styles.Success.Render("Setup Complete!"))
|
|
b.WriteString("\n\n")
|
|
b.WriteString(m.styles.Success.Render("✓ All packages installed and configurations deployed."))
|
|
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.WriteByte('\n')
|
|
}
|
|
|
|
b.WriteByte('\n')
|
|
b.WriteString(m.styles.Normal.Render("Your system is ready! Log out and log back in to start using\nyour new desktop environment.\n" + m.loginHint()))
|
|
b.WriteString("\n\n")
|
|
|
|
label := m.styles.Subtle
|
|
cmd := m.styles.Accent
|
|
autostart, logs := m.troubleshootingHints()
|
|
|
|
b.WriteString(label.Render("Troubleshooting:") + "\n")
|
|
b.WriteString(label.Render(" Disable autostart: ") + cmd.Render(autostart) + "\n")
|
|
b.WriteString(label.Render(" View logs: ") + cmd.Render(logs) + "\n")
|
|
|
|
if m.osInfo != nil {
|
|
if uninstall := uninstallCommand(m.osInfo.Distribution.ID, m.dependencies); uninstall != "" {
|
|
b.WriteString(label.Render(" Uninstall: ") + cmd.Render(uninstall) + "\n")
|
|
}
|
|
}
|
|
|
|
b.WriteByte('\n')
|
|
b.WriteString(m.styles.Normal.Render("Press Enter to exit."))
|
|
|
|
if m.logFilePath != "" {
|
|
b.WriteString("\n\n")
|
|
b.WriteString(m.styles.Subtle.Render(fmt.Sprintf("Full logs: %s", m.logFilePath)))
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func (m Model) viewError() string {
|
|
var b strings.Builder
|
|
|
|
b.WriteString(m.renderBanner())
|
|
b.WriteByte('\n')
|
|
b.WriteString(m.styles.Error.Render("Installation Failed"))
|
|
b.WriteString("\n\n")
|
|
|
|
if m.err != nil {
|
|
b.WriteString(m.styles.Error.Render(wrapText("✗ "+m.err.Error(), 80)))
|
|
b.WriteString("\n\n")
|
|
}
|
|
|
|
if m.packageProgress.error != nil {
|
|
b.WriteString(m.styles.Error.Render(wrapText("Package Installation Error: "+m.packageProgress.error.Error(), 80)))
|
|
b.WriteString("\n\n")
|
|
}
|
|
|
|
if len(m.installationLogs) > 0 {
|
|
m.writeRecentLogs(&b, m.styles.Warning.Render("Installation Logs (last 15 lines):"), 15)
|
|
b.WriteByte('\n')
|
|
}
|
|
|
|
b.WriteString(m.styles.Subtle.Render("Press Ctrl+D for full debug logs"))
|
|
b.WriteByte('\n')
|
|
|
|
if m.logFilePath != "" {
|
|
b.WriteByte('\n')
|
|
b.WriteString(m.styles.Warning.Render(fmt.Sprintf("Full logs: %s", m.logFilePath)))
|
|
b.WriteByte('\n')
|
|
}
|
|
|
|
b.WriteString(m.styles.Subtle.Render("Press Enter to exit"))
|
|
return b.String()
|
|
}
|
|
|
|
func (m Model) updateInstallingPackagesState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
progressMsg, ok := msg.(packageInstallProgressMsg)
|
|
if !ok {
|
|
return m, m.listenForLogs()
|
|
}
|
|
|
|
m.packageProgress = progressMsg
|
|
|
|
if progressMsg.logOutput != "" {
|
|
m.installationLogs = append(m.installationLogs, progressMsg.logOutput)
|
|
m.installationLogs = tailLines(m.installationLogs, 50)
|
|
}
|
|
|
|
if !progressMsg.isComplete {
|
|
return m, m.listenForPackageProgress()
|
|
}
|
|
|
|
if progressMsg.error != nil {
|
|
m.state = StateError
|
|
m.isLoading = false
|
|
return m, m.listenForPackageProgress()
|
|
}
|
|
|
|
m.installationLogs = nil
|
|
m.state = StateConfigConfirmation
|
|
m.isLoading = true
|
|
return m, tea.Batch(m.spinner.Tick, m.checkExistingConfigurations())
|
|
}
|
|
|
|
func (m Model) updateInstallCompleteState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.String() == "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 && keyMsg.String() == "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{}
|
|
}
|
|
return msg
|
|
}
|
|
}
|
|
|
|
func (m Model) viewDebugLogs() string {
|
|
var b strings.Builder
|
|
|
|
b.WriteString(m.styles.Highlight.Render("Debug Logs"))
|
|
b.WriteString("\n\n")
|
|
|
|
allLogs := append(append([]string{}, m.logMessages...), m.installationLogs...)
|
|
if len(allLogs) == 0 {
|
|
b.WriteString("No logs available\n")
|
|
} else {
|
|
maxHeight := max(m.height-6, 10)
|
|
startIdx := max(len(allLogs)-maxHeight, 0)
|
|
|
|
for i := startIdx; i < len(allLogs); i++ {
|
|
if allLogs[i] != "" {
|
|
fmt.Fprintf(&b, "%d: %s\n", i, allLogs[i])
|
|
}
|
|
}
|
|
|
|
if startIdx > 0 {
|
|
b.WriteString(m.styles.Subtle.Render(fmt.Sprintf("... (%d older log entries hidden)\n", startIdx)))
|
|
}
|
|
}
|
|
|
|
b.WriteByte('\n')
|
|
b.WriteString(m.styles.Accent.Render("Press Ctrl+D to return, Ctrl+C to quit"))
|
|
return b.String()
|
|
}
|