mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-08 06:25:37 -05:00
rename backend to core
This commit is contained in:
438
core/internal/dms/app.go
Normal file
438
core/internal/dms/app.go
Normal file
@@ -0,0 +1,438 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type AppState int
|
||||
|
||||
const (
|
||||
StateMainMenu AppState = iota
|
||||
StateUpdate
|
||||
StateUpdatePassword
|
||||
StateUpdateProgress
|
||||
StateShell
|
||||
StatePluginsMenu
|
||||
StatePluginsBrowse
|
||||
StatePluginDetail
|
||||
StatePluginSearch
|
||||
StatePluginsInstalled
|
||||
StatePluginInstalledDetail
|
||||
StateGreeterMenu
|
||||
StateGreeterCompositorSelect
|
||||
StateGreeterPassword
|
||||
StateGreeterInstalling
|
||||
StateAbout
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
version string
|
||||
detector *Detector
|
||||
dependencies []DependencyInfo
|
||||
state AppState
|
||||
selectedItem int
|
||||
width int
|
||||
height int
|
||||
|
||||
// Menu items
|
||||
menuItems []MenuItem
|
||||
|
||||
updateDeps []DependencyInfo
|
||||
selectedUpdateDep int
|
||||
updateToggles map[string]bool
|
||||
|
||||
updateProgressChan chan updateProgressMsg
|
||||
updateProgress updateProgressMsg
|
||||
updateLogs []string
|
||||
sudoPassword string
|
||||
passwordInput string
|
||||
passwordError string
|
||||
|
||||
// Window manager states
|
||||
hyprlandInstalled bool
|
||||
niriInstalled bool
|
||||
|
||||
selectedGreeterItem int
|
||||
greeterInstallChan chan greeterProgressMsg
|
||||
greeterProgress greeterProgressMsg
|
||||
greeterLogs []string
|
||||
greeterPasswordInput string
|
||||
greeterPasswordError string
|
||||
greeterSudoPassword string
|
||||
greeterCompositors []string
|
||||
greeterSelectedComp int
|
||||
greeterChosenCompositor string
|
||||
|
||||
pluginsMenuItems []MenuItem
|
||||
selectedPluginsMenuItem int
|
||||
pluginsList []pluginInfo
|
||||
filteredPluginsList []pluginInfo
|
||||
selectedPluginIndex int
|
||||
pluginsLoading bool
|
||||
pluginsError string
|
||||
pluginSearchQuery string
|
||||
installedPluginsList []pluginInfo
|
||||
selectedInstalledIndex int
|
||||
installedPluginsLoading bool
|
||||
installedPluginsError string
|
||||
pluginInstallStatus map[string]bool
|
||||
}
|
||||
|
||||
type pluginInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Category string
|
||||
Author string
|
||||
Description string
|
||||
Repo string
|
||||
Path string
|
||||
Capabilities []string
|
||||
Compositors []string
|
||||
Dependencies []string
|
||||
FirstParty bool
|
||||
}
|
||||
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
Action AppState
|
||||
}
|
||||
|
||||
func NewModel(version string) Model {
|
||||
detector, _ := NewDetector()
|
||||
dependencies := detector.GetInstalledComponents()
|
||||
|
||||
// Use the proper detection method for both window managers
|
||||
hyprlandInstalled, niriInstalled, err := detector.GetWindowManagerStatus()
|
||||
if err != nil {
|
||||
// Fallback to false if detection fails
|
||||
hyprlandInstalled = false
|
||||
niriInstalled = false
|
||||
}
|
||||
|
||||
updateToggles := make(map[string]bool)
|
||||
for _, dep := range dependencies {
|
||||
if dep.Name == "dms (DankMaterialShell)" && dep.Status == deps.StatusNeedsUpdate {
|
||||
updateToggles[dep.Name] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
m := Model{
|
||||
version: version,
|
||||
detector: detector,
|
||||
dependencies: dependencies,
|
||||
state: StateMainMenu,
|
||||
selectedItem: 0,
|
||||
updateToggles: updateToggles,
|
||||
updateDeps: dependencies,
|
||||
updateProgressChan: make(chan updateProgressMsg, 100),
|
||||
hyprlandInstalled: hyprlandInstalled,
|
||||
niriInstalled: niriInstalled,
|
||||
greeterInstallChan: make(chan greeterProgressMsg, 100),
|
||||
pluginInstallStatus: make(map[string]bool),
|
||||
}
|
||||
|
||||
m.menuItems = m.buildMenuItems()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Model) buildMenuItems() []MenuItem {
|
||||
items := []MenuItem{
|
||||
{Label: "Update", Action: StateUpdate},
|
||||
}
|
||||
|
||||
// Shell management
|
||||
if m.isShellRunning() {
|
||||
items = append(items, MenuItem{Label: "Terminate Shell", Action: StateShell})
|
||||
} else {
|
||||
items = append(items, MenuItem{Label: "Start Shell (Daemon)", Action: StateShell})
|
||||
}
|
||||
|
||||
// Plugins management
|
||||
items = append(items, MenuItem{Label: "Plugins", Action: StatePluginsMenu})
|
||||
|
||||
// Greeter management
|
||||
items = append(items, MenuItem{Label: "Greeter", Action: StateGreeterMenu})
|
||||
|
||||
items = append(items, MenuItem{Label: "About", Action: StateAbout})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (m *Model) buildPluginsMenuItems() []MenuItem {
|
||||
return []MenuItem{
|
||||
{Label: "Browse Plugins", Action: StatePluginsBrowse},
|
||||
{Label: "View Installed", Action: StatePluginsInstalled},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) isShellRunning() bool {
|
||||
// Check for both -c and -p flag patterns since quickshell can be started either way
|
||||
// -c dms: config name mode
|
||||
// -p <path>/dms: path mode (used when installed via system packages)
|
||||
cmd := exec.Command("pgrep", "-f", "qs.*dms")
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
case shellStartedMsg:
|
||||
m.menuItems = m.buildMenuItems()
|
||||
if m.selectedItem >= len(m.menuItems) {
|
||||
m.selectedItem = len(m.menuItems) - 1
|
||||
}
|
||||
return m, nil
|
||||
case updateProgressMsg:
|
||||
m.updateProgress = msg
|
||||
if msg.logOutput != "" {
|
||||
m.updateLogs = append(m.updateLogs, msg.logOutput)
|
||||
}
|
||||
return m, m.waitForProgress()
|
||||
case updateCompleteMsg:
|
||||
m.updateProgress.complete = true
|
||||
m.updateProgress.err = msg.err
|
||||
m.dependencies = m.detector.GetInstalledComponents()
|
||||
m.updateDeps = m.dependencies
|
||||
m.menuItems = m.buildMenuItems()
|
||||
|
||||
// Restart shell if update was successful and shell is running
|
||||
if msg.err == nil && m.isShellRunning() {
|
||||
restartShell()
|
||||
}
|
||||
return m, nil
|
||||
case greeterProgressMsg:
|
||||
m.greeterProgress = msg
|
||||
if msg.logOutput != "" {
|
||||
m.greeterLogs = append(m.greeterLogs, msg.logOutput)
|
||||
}
|
||||
return m, m.waitForGreeterProgress()
|
||||
case pluginsLoadedMsg:
|
||||
m.pluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.pluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
m.updatePluginInstallStatus()
|
||||
}
|
||||
return m, nil
|
||||
case installedPluginsLoadedMsg:
|
||||
m.installedPluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.installedPluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.installedPluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.selectedInstalledIndex = 0
|
||||
}
|
||||
return m, nil
|
||||
case pluginUninstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
m.state = StatePluginInstalledDetail
|
||||
} else {
|
||||
m.state = StatePluginsInstalled
|
||||
m.installedPluginsLoading = true
|
||||
m.installedPluginsError = ""
|
||||
return m, loadInstalledPlugins
|
||||
}
|
||||
return m, nil
|
||||
case pluginInstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginInstallStatus[msg.pluginName] = true
|
||||
m.pluginsError = ""
|
||||
}
|
||||
return m, nil
|
||||
case greeterPasswordValidMsg:
|
||||
if msg.valid {
|
||||
m.greeterSudoPassword = msg.password
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
m.state = StateGreeterInstalling
|
||||
m.greeterProgress = greeterProgressMsg{step: "Starting greeter installation..."}
|
||||
m.greeterLogs = []string{}
|
||||
return m, tea.Batch(m.performGreeterInstall(), m.waitForGreeterProgress())
|
||||
} else {
|
||||
m.greeterPasswordError = "Incorrect password. Please try again."
|
||||
m.greeterPasswordInput = ""
|
||||
}
|
||||
return m, nil
|
||||
case passwordValidMsg:
|
||||
if msg.valid {
|
||||
m.sudoPassword = msg.password
|
||||
m.passwordInput = ""
|
||||
m.passwordError = ""
|
||||
m.state = StateUpdateProgress
|
||||
m.updateProgress = updateProgressMsg{progress: 0.0, step: "Starting update..."}
|
||||
m.updateLogs = []string{}
|
||||
return m, tea.Batch(m.performUpdate(), m.waitForProgress())
|
||||
} else {
|
||||
m.passwordError = "Incorrect password. Please try again."
|
||||
m.passwordInput = ""
|
||||
}
|
||||
return m, nil
|
||||
case tea.KeyMsg:
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.updateMainMenu(msg)
|
||||
case StateUpdate:
|
||||
return m.updateUpdateView(msg)
|
||||
case StateUpdatePassword:
|
||||
return m.updatePasswordView(msg)
|
||||
case StateUpdateProgress:
|
||||
return m.updateProgressView(msg)
|
||||
case StateShell:
|
||||
return m.updateShellView(msg)
|
||||
case StatePluginsMenu:
|
||||
return m.updatePluginsMenu(msg)
|
||||
case StatePluginsBrowse:
|
||||
return m.updatePluginsBrowse(msg)
|
||||
case StatePluginDetail:
|
||||
return m.updatePluginDetail(msg)
|
||||
case StatePluginSearch:
|
||||
return m.updatePluginSearch(msg)
|
||||
case StatePluginsInstalled:
|
||||
return m.updatePluginsInstalled(msg)
|
||||
case StatePluginInstalledDetail:
|
||||
return m.updatePluginInstalledDetail(msg)
|
||||
case StateGreeterMenu:
|
||||
return m.updateGreeterMenu(msg)
|
||||
case StateGreeterCompositorSelect:
|
||||
return m.updateGreeterCompositorSelect(msg)
|
||||
case StateGreeterPassword:
|
||||
return m.updateGreeterPasswordView(msg)
|
||||
case StateGreeterInstalling:
|
||||
return m.updateGreeterInstalling(msg)
|
||||
case StateAbout:
|
||||
return m.updateAboutView(msg)
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
type updateProgressMsg struct {
|
||||
progress float64
|
||||
step string
|
||||
complete bool
|
||||
err error
|
||||
logOutput string
|
||||
}
|
||||
|
||||
type updateCompleteMsg struct {
|
||||
err error
|
||||
}
|
||||
|
||||
type passwordValidMsg struct {
|
||||
password string
|
||||
valid bool
|
||||
}
|
||||
|
||||
type greeterProgressMsg struct {
|
||||
step string
|
||||
complete bool
|
||||
err error
|
||||
logOutput string
|
||||
}
|
||||
|
||||
type greeterPasswordValidMsg struct {
|
||||
password string
|
||||
valid bool
|
||||
}
|
||||
|
||||
func (m Model) waitForProgress() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return <-m.updateProgressChan
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) waitForGreeterProgress() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return <-m.greeterInstallChan
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.renderMainMenu()
|
||||
case StateUpdate:
|
||||
return m.renderUpdateView()
|
||||
case StateUpdatePassword:
|
||||
return m.renderPasswordView()
|
||||
case StateUpdateProgress:
|
||||
return m.renderProgressView()
|
||||
case StateShell:
|
||||
return m.renderShellView()
|
||||
case StatePluginsMenu:
|
||||
return m.renderPluginsMenu()
|
||||
case StatePluginsBrowse:
|
||||
return m.renderPluginsBrowse()
|
||||
case StatePluginDetail:
|
||||
return m.renderPluginDetail()
|
||||
case StatePluginSearch:
|
||||
return m.renderPluginSearch()
|
||||
case StatePluginsInstalled:
|
||||
return m.renderPluginsInstalled()
|
||||
case StatePluginInstalledDetail:
|
||||
return m.renderPluginInstalledDetail()
|
||||
case StateGreeterMenu:
|
||||
return m.renderGreeterMenu()
|
||||
case StateGreeterCompositorSelect:
|
||||
return m.renderGreeterCompositorSelect()
|
||||
case StateGreeterPassword:
|
||||
return m.renderGreeterPasswordView()
|
||||
case StateGreeterInstalling:
|
||||
return m.renderGreeterInstalling()
|
||||
case StateAbout:
|
||||
return m.renderAboutView()
|
||||
default:
|
||||
return m.renderMainMenu()
|
||||
}
|
||||
}
|
||||
261
core/internal/dms/app_distro.go
Normal file
261
core/internal/dms/app_distro.go
Normal file
@@ -0,0 +1,261 @@
|
||||
//go:build distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type AppState int
|
||||
|
||||
const (
|
||||
StateMainMenu AppState = iota
|
||||
StateShell
|
||||
StatePluginsMenu
|
||||
StatePluginsBrowse
|
||||
StatePluginDetail
|
||||
StatePluginSearch
|
||||
StatePluginsInstalled
|
||||
StatePluginInstalledDetail
|
||||
StateAbout
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
version string
|
||||
detector *Detector
|
||||
dependencies []DependencyInfo
|
||||
state AppState
|
||||
selectedItem int
|
||||
width int
|
||||
height int
|
||||
|
||||
// Menu items
|
||||
menuItems []MenuItem
|
||||
|
||||
// Window manager states
|
||||
hyprlandInstalled bool
|
||||
niriInstalled bool
|
||||
|
||||
pluginsMenuItems []MenuItem
|
||||
selectedPluginsMenuItem int
|
||||
pluginsList []pluginInfo
|
||||
filteredPluginsList []pluginInfo
|
||||
selectedPluginIndex int
|
||||
pluginsLoading bool
|
||||
pluginsError string
|
||||
pluginSearchQuery string
|
||||
installedPluginsList []pluginInfo
|
||||
selectedInstalledIndex int
|
||||
installedPluginsLoading bool
|
||||
installedPluginsError string
|
||||
pluginInstallStatus map[string]bool
|
||||
}
|
||||
|
||||
type pluginInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Category string
|
||||
Author string
|
||||
Description string
|
||||
Repo string
|
||||
Path string
|
||||
Capabilities []string
|
||||
Compositors []string
|
||||
Dependencies []string
|
||||
FirstParty bool
|
||||
}
|
||||
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
Action AppState
|
||||
}
|
||||
|
||||
func NewModel(version string) Model {
|
||||
detector, _ := NewDetector()
|
||||
dependencies := detector.GetInstalledComponents()
|
||||
|
||||
// Use the proper detection method for both window managers
|
||||
hyprlandInstalled, niriInstalled, err := detector.GetWindowManagerStatus()
|
||||
if err != nil {
|
||||
// Fallback to false if detection fails
|
||||
hyprlandInstalled = false
|
||||
niriInstalled = false
|
||||
}
|
||||
|
||||
m := Model{
|
||||
version: version,
|
||||
detector: detector,
|
||||
dependencies: dependencies,
|
||||
state: StateMainMenu,
|
||||
selectedItem: 0,
|
||||
hyprlandInstalled: hyprlandInstalled,
|
||||
niriInstalled: niriInstalled,
|
||||
pluginInstallStatus: make(map[string]bool),
|
||||
}
|
||||
|
||||
m.menuItems = m.buildMenuItems()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Model) buildMenuItems() []MenuItem {
|
||||
items := []MenuItem{}
|
||||
|
||||
// Shell management
|
||||
if m.isShellRunning() {
|
||||
items = append(items, MenuItem{Label: "Terminate Shell", Action: StateShell})
|
||||
} else {
|
||||
items = append(items, MenuItem{Label: "Start Shell (Daemon)", Action: StateShell})
|
||||
}
|
||||
|
||||
// Plugins management
|
||||
items = append(items, MenuItem{Label: "Plugins", Action: StatePluginsMenu})
|
||||
|
||||
items = append(items, MenuItem{Label: "About", Action: StateAbout})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (m *Model) buildPluginsMenuItems() []MenuItem {
|
||||
return []MenuItem{
|
||||
{Label: "Browse Plugins", Action: StatePluginsBrowse},
|
||||
{Label: "View Installed", Action: StatePluginsInstalled},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) isShellRunning() bool {
|
||||
cmd := exec.Command("pgrep", "-f", "qs -c dms")
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
case pluginsLoadedMsg:
|
||||
m.pluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.pluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
m.updatePluginInstallStatus()
|
||||
}
|
||||
return m, nil
|
||||
case installedPluginsLoadedMsg:
|
||||
m.installedPluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.installedPluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.installedPluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.selectedInstalledIndex = 0
|
||||
}
|
||||
return m, nil
|
||||
case pluginUninstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
m.state = StatePluginInstalledDetail
|
||||
} else {
|
||||
m.state = StatePluginsInstalled
|
||||
m.installedPluginsLoading = true
|
||||
m.installedPluginsError = ""
|
||||
return m, loadInstalledPlugins
|
||||
}
|
||||
return m, nil
|
||||
case pluginInstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginInstallStatus[msg.pluginName] = true
|
||||
m.pluginsError = ""
|
||||
}
|
||||
return m, nil
|
||||
case tea.KeyMsg:
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.updateMainMenu(msg)
|
||||
case StateShell:
|
||||
return m.updateShellView(msg)
|
||||
case StatePluginsMenu:
|
||||
return m.updatePluginsMenu(msg)
|
||||
case StatePluginsBrowse:
|
||||
return m.updatePluginsBrowse(msg)
|
||||
case StatePluginDetail:
|
||||
return m.updatePluginDetail(msg)
|
||||
case StatePluginSearch:
|
||||
return m.updatePluginSearch(msg)
|
||||
case StatePluginsInstalled:
|
||||
return m.updatePluginsInstalled(msg)
|
||||
case StatePluginInstalledDetail:
|
||||
return m.updatePluginInstalledDetail(msg)
|
||||
case StateAbout:
|
||||
return m.updateAboutView(msg)
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.renderMainMenu()
|
||||
case StateShell:
|
||||
return m.renderShellView()
|
||||
case StatePluginsMenu:
|
||||
return m.renderPluginsMenu()
|
||||
case StatePluginsBrowse:
|
||||
return m.renderPluginsBrowse()
|
||||
case StatePluginDetail:
|
||||
return m.renderPluginDetail()
|
||||
case StatePluginSearch:
|
||||
return m.renderPluginSearch()
|
||||
case StatePluginsInstalled:
|
||||
return m.renderPluginsInstalled()
|
||||
case StatePluginInstalledDetail:
|
||||
return m.renderPluginInstalledDetail()
|
||||
case StateAbout:
|
||||
return m.renderAboutView()
|
||||
default:
|
||||
return m.renderMainMenu()
|
||||
}
|
||||
}
|
||||
167
core/internal/dms/detector.go
Normal file
167
core/internal/dms/detector.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
)
|
||||
|
||||
type Detector struct {
|
||||
homeDir string
|
||||
distribution distros.Distribution
|
||||
}
|
||||
|
||||
func (d *Detector) GetDistribution() distros.Distribution {
|
||||
return d.distribution
|
||||
}
|
||||
|
||||
func NewDetector() (*Detector, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logChan := make(chan string, 100)
|
||||
go func() {
|
||||
for range logChan {
|
||||
}
|
||||
}()
|
||||
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dist, err := distros.NewDistribution(osInfo.Distribution.ID, logChan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Detector{
|
||||
homeDir: homeDir,
|
||||
distribution: dist,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Detector) IsDMSInstalled() bool {
|
||||
_, err := config.LocateDMSConfig()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *Detector) GetDependencyStatus() ([]deps.Dependency, error) {
|
||||
hyprlandDeps, err := d.distribution.DetectDependencies(context.Background(), deps.WindowManagerHyprland)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
niriDeps, err := d.distribution.DetectDependencies(context.Background(), deps.WindowManagerNiri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Combine dependencies and deduplicate
|
||||
depMap := make(map[string]deps.Dependency)
|
||||
|
||||
for _, dep := range hyprlandDeps {
|
||||
depMap[dep.Name] = dep
|
||||
}
|
||||
|
||||
for _, dep := range niriDeps {
|
||||
// If dependency already exists, keep the one that's installed or needs update
|
||||
if existing, exists := depMap[dep.Name]; exists {
|
||||
if dep.Status > existing.Status {
|
||||
depMap[dep.Name] = dep
|
||||
}
|
||||
} else {
|
||||
depMap[dep.Name] = dep
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map back to slice
|
||||
var allDeps []deps.Dependency
|
||||
for _, dep := range depMap {
|
||||
allDeps = append(allDeps, dep)
|
||||
}
|
||||
|
||||
return allDeps, nil
|
||||
}
|
||||
|
||||
func (d *Detector) GetWindowManagerStatus() (bool, bool, error) {
|
||||
// Reuse the existing command detection logic from BaseDistribution
|
||||
// Since all distros embed BaseDistribution, we can access it via interface
|
||||
type CommandChecker interface {
|
||||
CommandExists(string) bool
|
||||
}
|
||||
|
||||
checker, ok := d.distribution.(CommandChecker)
|
||||
if !ok {
|
||||
// Fallback to direct command check if interface not available
|
||||
hyprlandInstalled := d.commandExists("hyprland") || d.commandExists("Hyprland")
|
||||
niriInstalled := d.commandExists("niri")
|
||||
return hyprlandInstalled, niriInstalled, nil
|
||||
}
|
||||
|
||||
hyprlandInstalled := checker.CommandExists("hyprland") || checker.CommandExists("Hyprland")
|
||||
niriInstalled := checker.CommandExists("niri")
|
||||
|
||||
return hyprlandInstalled, niriInstalled, nil
|
||||
}
|
||||
|
||||
func (d *Detector) commandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *Detector) GetInstalledComponents() []DependencyInfo {
|
||||
dependencies, err := d.GetDependencyStatus()
|
||||
if err != nil {
|
||||
return []DependencyInfo{}
|
||||
}
|
||||
|
||||
isNixOS := d.isNixOS()
|
||||
|
||||
var components []DependencyInfo
|
||||
for _, dep := range dependencies {
|
||||
// On NixOS, filter out the window managers themselves but keep their components
|
||||
if isNixOS && (dep.Name == "hyprland" || dep.Name == "niri") {
|
||||
continue
|
||||
}
|
||||
|
||||
components = append(components, DependencyInfo{
|
||||
Name: dep.Name,
|
||||
Status: dep.Status,
|
||||
Description: dep.Description,
|
||||
Required: dep.Required,
|
||||
})
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
func (d *Detector) isNixOS() bool {
|
||||
_, err := os.Stat("/etc/nixos")
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Alternative check
|
||||
if _, err := os.Stat("/nix/store"); err == nil {
|
||||
// Also check for nixos-version command
|
||||
if d.commandExists("nixos-version") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type DependencyInfo struct {
|
||||
Name string
|
||||
Status deps.DependencyStatus
|
||||
Description string
|
||||
Required bool
|
||||
}
|
||||
54
core/internal/dms/handlers_common.go
Normal file
54
core/internal/dms/handlers_common.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) updateShellView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
default:
|
||||
return m, tea.Quit
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateAboutView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q", "esc":
|
||||
if msg.String() == "esc" {
|
||||
m.state = StateMainMenu
|
||||
} else {
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func terminateShell() {
|
||||
patterns := []string{"dms run", "qs -c dms"}
|
||||
for _, pattern := range patterns {
|
||||
cmd := exec.Command("pkill", "-f", pattern)
|
||||
cmd.Run()
|
||||
}
|
||||
}
|
||||
|
||||
func startShellDaemon() {
|
||||
cmd := exec.Command("dms", "run", "-d")
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Errorf("Error starting daemon: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func restartShell() {
|
||||
terminateShell()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
startShellDaemon()
|
||||
}
|
||||
392
core/internal/dms/handlers_features.go
Normal file
392
core/internal/dms/handlers_features.go
Normal file
@@ -0,0 +1,392 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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) updateUpdateView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
filteredDeps := m.getFilteredDeps()
|
||||
maxIndex := len(filteredDeps) - 1
|
||||
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
case "up", "k":
|
||||
if m.selectedUpdateDep > 0 {
|
||||
m.selectedUpdateDep--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedUpdateDep < maxIndex {
|
||||
m.selectedUpdateDep++
|
||||
}
|
||||
case " ":
|
||||
if dep := m.getDepAtVisualIndex(m.selectedUpdateDep); dep != nil {
|
||||
m.updateToggles[dep.Name] = !m.updateToggles[dep.Name]
|
||||
}
|
||||
case "enter":
|
||||
hasSelected := false
|
||||
for _, toggle := range m.updateToggles {
|
||||
if toggle {
|
||||
hasSelected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasSelected {
|
||||
m.state = StateMainMenu
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.state = StateUpdatePassword
|
||||
m.passwordInput = ""
|
||||
m.passwordError = ""
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePasswordView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateUpdate
|
||||
m.passwordInput = ""
|
||||
m.passwordError = ""
|
||||
return m, nil
|
||||
case "enter":
|
||||
if m.passwordInput == "" {
|
||||
return m, nil
|
||||
}
|
||||
return m, m.validatePassword(m.passwordInput)
|
||||
case "backspace":
|
||||
if len(m.passwordInput) > 0 {
|
||||
m.passwordInput = m.passwordInput[:len(m.passwordInput)-1]
|
||||
}
|
||||
default:
|
||||
if len(msg.String()) == 1 && msg.String()[0] >= 32 && msg.String()[0] <= 126 {
|
||||
m.passwordInput += msg.String()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateProgressView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
if m.updateProgress.complete {
|
||||
m.state = StateMainMenu
|
||||
m.updateProgress = updateProgressMsg{}
|
||||
m.updateLogs = []string{}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) validatePassword(password string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
fmt.Fprintf(stdin, "%s\n", password)
|
||||
}()
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
outputStr := string(output)
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(outputStr, "Sorry, try again") ||
|
||||
strings.Contains(outputStr, "incorrect password") ||
|
||||
strings.Contains(outputStr, "authentication failure") {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
return passwordValidMsg{password: password, valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) performUpdate() tea.Cmd {
|
||||
var depsToUpdate []deps.Dependency
|
||||
|
||||
for _, depInfo := range m.updateDeps {
|
||||
if m.updateToggles[depInfo.Name] {
|
||||
depsToUpdate = append(depsToUpdate, deps.Dependency{
|
||||
Name: depInfo.Name,
|
||||
Status: depInfo.Status,
|
||||
Description: depInfo.Description,
|
||||
Required: depInfo.Required,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(depsToUpdate) == 0 {
|
||||
return func() tea.Msg {
|
||||
return updateCompleteMsg{err: nil}
|
||||
}
|
||||
}
|
||||
|
||||
wm := deps.WindowManagerHyprland
|
||||
if m.niriInstalled {
|
||||
wm = deps.WindowManagerNiri
|
||||
}
|
||||
|
||||
sudoPassword := m.sudoPassword
|
||||
reinstallFlags := make(map[string]bool)
|
||||
for name, toggled := range m.updateToggles {
|
||||
if toggled {
|
||||
reinstallFlags[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
distribution := m.detector.GetDistribution()
|
||||
progressChan := m.updateProgressChan
|
||||
|
||||
return func() tea.Msg {
|
||||
installerChan := make(chan distros.InstallProgressMsg, 100)
|
||||
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
disabledFlags := make(map[string]bool)
|
||||
err := distribution.InstallPackages(ctx, depsToUpdate, wm, sudoPassword, reinstallFlags, disabledFlags, false, installerChan)
|
||||
close(installerChan)
|
||||
|
||||
if err != nil {
|
||||
progressChan <- updateProgressMsg{complete: true, err: err}
|
||||
} else {
|
||||
progressChan <- updateProgressMsg{complete: true}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for msg := range installerChan {
|
||||
progressChan <- updateProgressMsg{
|
||||
progress: msg.Progress,
|
||||
step: msg.Step,
|
||||
complete: msg.IsComplete,
|
||||
err: msg.Error,
|
||||
logOutput: msg.LogOutput,
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
greeterMenuItems := []string{"Install Greeter"}
|
||||
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
case "up", "k":
|
||||
if m.selectedGreeterItem > 0 {
|
||||
m.selectedGreeterItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedGreeterItem < len(greeterMenuItems)-1 {
|
||||
m.selectedGreeterItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedGreeterItem == 0 {
|
||||
compositors := greeter.DetectCompositors()
|
||||
if len(compositors) == 0 {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.greeterCompositors = compositors
|
||||
|
||||
if len(compositors) > 1 {
|
||||
m.state = StateGreeterCompositorSelect
|
||||
m.greeterSelectedComp = 0
|
||||
return m, nil
|
||||
} else {
|
||||
m.greeterChosenCompositor = compositors[0]
|
||||
m.state = StateGreeterPassword
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterCompositorSelect(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateGreeterMenu
|
||||
return m, nil
|
||||
case "up", "k":
|
||||
if m.greeterSelectedComp > 0 {
|
||||
m.greeterSelectedComp--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.greeterSelectedComp < len(m.greeterCompositors)-1 {
|
||||
m.greeterSelectedComp++
|
||||
}
|
||||
case "enter", " ":
|
||||
m.greeterChosenCompositor = m.greeterCompositors[m.greeterSelectedComp]
|
||||
m.state = StateGreeterPassword
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterPasswordView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateGreeterMenu
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
return m, nil
|
||||
case "enter":
|
||||
if m.greeterPasswordInput == "" {
|
||||
return m, nil
|
||||
}
|
||||
return m, m.validateGreeterPassword(m.greeterPasswordInput)
|
||||
case "backspace":
|
||||
if len(m.greeterPasswordInput) > 0 {
|
||||
m.greeterPasswordInput = m.greeterPasswordInput[:len(m.greeterPasswordInput)-1]
|
||||
}
|
||||
default:
|
||||
if len(msg.String()) == 1 && msg.String()[0] >= 32 && msg.String()[0] <= 126 {
|
||||
m.greeterPasswordInput += msg.String()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterInstalling(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
if m.greeterProgress.complete {
|
||||
m.state = StateMainMenu
|
||||
m.greeterProgress = greeterProgressMsg{}
|
||||
m.greeterLogs = []string{}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) performGreeterInstall() tea.Cmd {
|
||||
progressChan := m.greeterInstallChan
|
||||
sudoPassword := m.greeterSudoPassword
|
||||
compositor := m.greeterChosenCompositor
|
||||
|
||||
return func() tea.Msg {
|
||||
go func() {
|
||||
logFunc := func(msg string) {
|
||||
progressChan <- greeterProgressMsg{step: msg, logOutput: msg}
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Checking greetd installation..."}
|
||||
if err := performGreeterInstallSteps(progressChan, logFunc, sudoPassword, compositor); err != nil {
|
||||
progressChan <- greeterProgressMsg{step: "Installation failed", complete: true, err: err}
|
||||
return
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Installation complete", complete: true}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) validateGreeterPassword(password string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return greeterPasswordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
fmt.Fprintf(stdin, "%s\n", password)
|
||||
}()
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
outputStr := string(output)
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(outputStr, "Sorry, try again") ||
|
||||
strings.Contains(outputStr, "incorrect password") ||
|
||||
strings.Contains(outputStr, "authentication failure") {
|
||||
return greeterPasswordValidMsg{password: "", valid: false}
|
||||
}
|
||||
return greeterPasswordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
return greeterPasswordValidMsg{password: password, valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
func performGreeterInstallSteps(progressChan chan greeterProgressMsg, logFunc func(string), sudoPassword string, compositor string) error {
|
||||
if err := greeter.EnsureGreetdInstalled(logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Detecting DMS installation..."}
|
||||
dmsPath, err := greeter.DetectDMSPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ Found DMS at: %s", dmsPath))
|
||||
|
||||
logFunc(fmt.Sprintf("✓ Selected compositor: %s", compositor))
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Copying greeter files..."}
|
||||
if err := greeter.CopyGreeterFiles(dmsPath, compositor, logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Configuring greetd..."}
|
||||
if err := greeter.ConfigureGreetd(dmsPath, compositor, logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Synchronizing DMS configurations..."}
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
61
core/internal/dms/handlers_mainmenu.go
Normal file
61
core/internal/dms/handlers_mainmenu.go
Normal file
@@ -0,0 +1,61 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type shellStartedMsg struct{}
|
||||
|
||||
func (m Model) updateMainMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q", "esc":
|
||||
return m, tea.Quit
|
||||
case "up", "k":
|
||||
if m.selectedItem > 0 {
|
||||
m.selectedItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedItem < len(m.menuItems)-1 {
|
||||
m.selectedItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedItem < len(m.menuItems) {
|
||||
selectedAction := m.menuItems[m.selectedItem].Action
|
||||
selectedLabel := m.menuItems[m.selectedItem].Label
|
||||
|
||||
switch selectedAction {
|
||||
case StateUpdate:
|
||||
m.state = StateUpdate
|
||||
m.selectedUpdateDep = 0
|
||||
case StateShell:
|
||||
if selectedLabel == "Terminate Shell" {
|
||||
terminateShell()
|
||||
m.menuItems = m.buildMenuItems()
|
||||
if m.selectedItem >= len(m.menuItems) {
|
||||
m.selectedItem = len(m.menuItems) - 1
|
||||
}
|
||||
} else {
|
||||
startShellDaemon()
|
||||
// Wait a moment for the daemon to actually start before checking status
|
||||
return m, tea.Tick(300*time.Millisecond, func(t time.Time) tea.Msg {
|
||||
return shellStartedMsg{}
|
||||
})
|
||||
}
|
||||
case StatePluginsMenu:
|
||||
m.state = StatePluginsMenu
|
||||
m.selectedPluginsMenuItem = 0
|
||||
m.pluginsMenuItems = m.buildPluginsMenuItems()
|
||||
case StateGreeterMenu:
|
||||
m.state = StateGreeterMenu
|
||||
m.selectedGreeterItem = 0
|
||||
case StateAbout:
|
||||
m.state = StateAbout
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
55
core/internal/dms/handlers_mainmenu_distro.go
Normal file
55
core/internal/dms/handlers_mainmenu_distro.go
Normal file
@@ -0,0 +1,55 @@
|
||||
//go:build distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type shellStartedMsg struct{}
|
||||
|
||||
func (m Model) updateMainMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q", "esc":
|
||||
return m, tea.Quit
|
||||
case "up", "k":
|
||||
if m.selectedItem > 0 {
|
||||
m.selectedItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedItem < len(m.menuItems)-1 {
|
||||
m.selectedItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedItem < len(m.menuItems) {
|
||||
selectedAction := m.menuItems[m.selectedItem].Action
|
||||
selectedLabel := m.menuItems[m.selectedItem].Label
|
||||
|
||||
switch selectedAction {
|
||||
case StateShell:
|
||||
if selectedLabel == "Terminate Shell" {
|
||||
terminateShell()
|
||||
m.menuItems = m.buildMenuItems()
|
||||
if m.selectedItem >= len(m.menuItems) {
|
||||
m.selectedItem = len(m.menuItems) - 1
|
||||
}
|
||||
} else {
|
||||
startShellDaemon()
|
||||
// Wait a moment for the daemon to actually start before checking status
|
||||
return m, tea.Tick(300*time.Millisecond, func(t time.Time) tea.Msg {
|
||||
return shellStartedMsg{}
|
||||
})
|
||||
}
|
||||
case StatePluginsMenu:
|
||||
m.state = StatePluginsMenu
|
||||
m.selectedPluginsMenuItem = 0
|
||||
m.pluginsMenuItems = m.buildPluginsMenuItems()
|
||||
case StateAbout:
|
||||
m.state = StateAbout
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
339
core/internal/dms/plugins_handlers.go
Normal file
339
core/internal/dms/plugins_handlers.go
Normal file
@@ -0,0 +1,339 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) updatePluginsMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
case "up", "k":
|
||||
if m.selectedPluginsMenuItem > 0 {
|
||||
m.selectedPluginsMenuItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedPluginsMenuItem < len(m.pluginsMenuItems)-1 {
|
||||
m.selectedPluginsMenuItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedPluginsMenuItem < len(m.pluginsMenuItems) {
|
||||
selectedAction := m.pluginsMenuItems[m.selectedPluginsMenuItem].Action
|
||||
switch selectedAction {
|
||||
case StatePluginsBrowse:
|
||||
m.state = StatePluginsBrowse
|
||||
m.pluginsLoading = true
|
||||
m.pluginsError = ""
|
||||
m.pluginsList = nil
|
||||
return m, loadPlugins
|
||||
case StatePluginsInstalled:
|
||||
m.state = StatePluginsInstalled
|
||||
m.installedPluginsLoading = true
|
||||
m.installedPluginsError = ""
|
||||
m.installedPluginsList = nil
|
||||
return m, loadInstalledPlugins
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginsBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsMenu
|
||||
m.pluginSearchQuery = ""
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
case "up", "k":
|
||||
if m.selectedPluginIndex > 0 {
|
||||
m.selectedPluginIndex--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedPluginIndex < len(m.filteredPluginsList)-1 {
|
||||
m.selectedPluginIndex++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedPluginIndex < len(m.filteredPluginsList) {
|
||||
m.state = StatePluginDetail
|
||||
}
|
||||
case "/":
|
||||
m.state = StatePluginSearch
|
||||
m.pluginSearchQuery = ""
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginDetail(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsBrowse
|
||||
case "i":
|
||||
if m.selectedPluginIndex < len(m.filteredPluginsList) {
|
||||
plugin := m.filteredPluginsList[m.selectedPluginIndex]
|
||||
installed := m.pluginInstallStatus[plugin.Name]
|
||||
if !installed {
|
||||
return m, installPlugin(plugin)
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginSearch(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsBrowse
|
||||
m.pluginSearchQuery = ""
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
case "enter":
|
||||
m.state = StatePluginsBrowse
|
||||
m.filterPlugins()
|
||||
case "backspace":
|
||||
if len(m.pluginSearchQuery) > 0 {
|
||||
m.pluginSearchQuery = m.pluginSearchQuery[:len(m.pluginSearchQuery)-1]
|
||||
}
|
||||
default:
|
||||
if len(msg.String()) == 1 {
|
||||
m.pluginSearchQuery += msg.String()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) filterPlugins() {
|
||||
if m.pluginSearchQuery == "" {
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
return
|
||||
}
|
||||
|
||||
rawPlugins := make([]plugins.Plugin, len(m.pluginsList))
|
||||
for i, p := range m.pluginsList {
|
||||
rawPlugins[i] = plugins.Plugin{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
}
|
||||
}
|
||||
|
||||
searchResults := plugins.FuzzySearch(m.pluginSearchQuery, rawPlugins)
|
||||
searchResults = plugins.SortByFirstParty(searchResults)
|
||||
|
||||
filtered := make([]pluginInfo, len(searchResults))
|
||||
for i, p := range searchResults {
|
||||
filtered[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
|
||||
m.filteredPluginsList = filtered
|
||||
m.selectedPluginIndex = 0
|
||||
}
|
||||
|
||||
type pluginsLoadedMsg struct {
|
||||
plugins []plugins.Plugin
|
||||
err error
|
||||
}
|
||||
|
||||
func loadPlugins() tea.Msg {
|
||||
registry, err := plugins.NewRegistry()
|
||||
if err != nil {
|
||||
return pluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
pluginList, err := registry.List()
|
||||
if err != nil {
|
||||
return pluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
return pluginsLoadedMsg{plugins: pluginList}
|
||||
}
|
||||
|
||||
func (m *Model) updatePluginInstallStatus() {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, plugin := range m.pluginsList {
|
||||
p := plugins.Plugin{ID: plugin.ID}
|
||||
installed, err := manager.IsInstalled(p)
|
||||
if err == nil {
|
||||
m.pluginInstallStatus[plugin.Name] = installed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) updatePluginsInstalled(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsMenu
|
||||
case "up", "k":
|
||||
if m.selectedInstalledIndex > 0 {
|
||||
m.selectedInstalledIndex--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedInstalledIndex < len(m.installedPluginsList)-1 {
|
||||
m.selectedInstalledIndex++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedInstalledIndex < len(m.installedPluginsList) {
|
||||
m.state = StatePluginInstalledDetail
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginInstalledDetail(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsInstalled
|
||||
case "u":
|
||||
if m.selectedInstalledIndex < len(m.installedPluginsList) {
|
||||
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
||||
return m, uninstallPlugin(plugin)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
type installedPluginsLoadedMsg struct {
|
||||
plugins []plugins.Plugin
|
||||
err error
|
||||
}
|
||||
|
||||
type pluginUninstalledMsg struct {
|
||||
pluginName string
|
||||
err error
|
||||
}
|
||||
|
||||
type pluginInstalledMsg struct {
|
||||
pluginName string
|
||||
err error
|
||||
}
|
||||
|
||||
func loadInstalledPlugins() tea.Msg {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
registry, err := plugins.NewRegistry()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
installedNames, err := manager.ListInstalled()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
allPlugins, err := registry.List()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
var installed []plugins.Plugin
|
||||
for _, id := range installedNames {
|
||||
for _, p := range allPlugins {
|
||||
if p.ID == id {
|
||||
installed = append(installed, p)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
installed = plugins.SortByFirstParty(installed)
|
||||
|
||||
return installedPluginsLoadedMsg{plugins: installed}
|
||||
}
|
||||
|
||||
func installPlugin(plugin pluginInfo) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return pluginInstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
p := plugins.Plugin{
|
||||
ID: plugin.ID,
|
||||
Name: plugin.Name,
|
||||
Category: plugin.Category,
|
||||
Author: plugin.Author,
|
||||
Description: plugin.Description,
|
||||
Repo: plugin.Repo,
|
||||
Path: plugin.Path,
|
||||
Capabilities: plugin.Capabilities,
|
||||
Compositors: plugin.Compositors,
|
||||
Dependencies: plugin.Dependencies,
|
||||
}
|
||||
|
||||
if err := manager.Install(p); err != nil {
|
||||
return pluginInstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
return pluginInstalledMsg{pluginName: plugin.Name}
|
||||
}
|
||||
}
|
||||
|
||||
func uninstallPlugin(plugin pluginInfo) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return pluginUninstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
p := plugins.Plugin{
|
||||
ID: plugin.ID,
|
||||
Name: plugin.Name,
|
||||
Category: plugin.Category,
|
||||
Author: plugin.Author,
|
||||
Description: plugin.Description,
|
||||
Repo: plugin.Repo,
|
||||
Path: plugin.Path,
|
||||
Capabilities: plugin.Capabilities,
|
||||
Compositors: plugin.Compositors,
|
||||
Dependencies: plugin.Dependencies,
|
||||
}
|
||||
|
||||
if err := manager.Uninstall(p); err != nil {
|
||||
return pluginUninstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
return pluginUninstalledMsg{pluginName: plugin.Name}
|
||||
}
|
||||
}
|
||||
367
core/internal/dms/plugins_views.go
Normal file
367
core/internal/dms/plugins_views.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) renderPluginsMenu() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
b.WriteString(titleStyle.Render("Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
for i, item := range m.pluginsMenuItems {
|
||||
if i == m.selectedPluginsMenuItem {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("→ %s", item.Label)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", item.Label)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("↑/↓: Navigate | Enter: Select | Esc: Back | q: Quit"))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginsBrowse() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
b.WriteString(titleStyle.Render("Browse Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if m.pluginsLoading {
|
||||
b.WriteString(normalStyle.Render("Fetching plugins from registry..."))
|
||||
} else if m.pluginsError != "" {
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s", m.pluginsError)))
|
||||
} else if len(m.filteredPluginsList) == 0 {
|
||||
if m.pluginSearchQuery != "" {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf("No plugins match '%s'", m.pluginSearchQuery)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render("No plugins found in registry."))
|
||||
}
|
||||
} else {
|
||||
installedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
for i, plugin := range m.filteredPluginsList {
|
||||
installed := m.pluginInstallStatus[plugin.Name]
|
||||
installMarker := ""
|
||||
if installed {
|
||||
installMarker = " [Installed]"
|
||||
}
|
||||
|
||||
if i == m.selectedPluginIndex {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("→ %s", plugin.Name)))
|
||||
if installed {
|
||||
b.WriteString(installedStyle.Render(installMarker))
|
||||
}
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", plugin.Name)))
|
||||
if installed {
|
||||
b.WriteString(installedStyle.Render(installMarker))
|
||||
}
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if m.pluginsLoading || m.pluginsError != "" {
|
||||
b.WriteString(instructionStyle.Render("Esc: Back | q: Quit"))
|
||||
} else {
|
||||
b.WriteString(instructionStyle.Render("↑/↓: Navigate | Enter: View/Install | /: Search | Esc: Back | q: Quit"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginDetail() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
labelStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if m.selectedPluginIndex >= len(m.filteredPluginsList) {
|
||||
return "No plugin selected"
|
||||
}
|
||||
|
||||
plugin := m.filteredPluginsList[m.selectedPluginIndex]
|
||||
|
||||
b.WriteString(titleStyle.Render(plugin.Name))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("ID: "))
|
||||
b.WriteString(normalStyle.Render(plugin.ID))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Category: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Category))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Author: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Author))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Description:"))
|
||||
b.WriteString("\n")
|
||||
wrapped := wrapText(plugin.Description, 60)
|
||||
b.WriteString(normalStyle.Render(wrapped))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Repository: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Repo))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(plugin.Capabilities) > 0 {
|
||||
b.WriteString(labelStyle.Render("Capabilities: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Capabilities, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Compositors) > 0 {
|
||||
b.WriteString(labelStyle.Render("Compositors: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Compositors, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Dependencies) > 0 {
|
||||
b.WriteString(labelStyle.Render("Dependencies: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Dependencies, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
installed := m.pluginInstallStatus[plugin.Name]
|
||||
if installed {
|
||||
b.WriteString(labelStyle.Render("Status: "))
|
||||
installedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
b.WriteString(installedStyle.Render("Installed"))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if installed {
|
||||
b.WriteString(instructionStyle.Render("Esc: Back | q: Quit"))
|
||||
} else {
|
||||
b.WriteString(instructionStyle.Render("i: Install | Esc: Back | q: Quit"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginSearch() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(titleStyle.Render("Search Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(normalStyle.Render("Query: "))
|
||||
b.WriteString(titleStyle.Render(m.pluginSearchQuery + "▌"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Enter: Search | Esc: Cancel"))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginsInstalled() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
b.WriteString(titleStyle.Render("Installed Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if m.installedPluginsLoading {
|
||||
b.WriteString(normalStyle.Render("Loading installed plugins..."))
|
||||
} else if m.installedPluginsError != "" {
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s", m.installedPluginsError)))
|
||||
} else if len(m.installedPluginsList) == 0 {
|
||||
b.WriteString(normalStyle.Render("No plugins installed."))
|
||||
} else {
|
||||
for i, plugin := range m.installedPluginsList {
|
||||
if i == m.selectedInstalledIndex {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("→ %s", plugin.Name)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", plugin.Name)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if m.installedPluginsLoading || m.installedPluginsError != "" {
|
||||
b.WriteString(instructionStyle.Render("Esc: Back | q: Quit"))
|
||||
} else {
|
||||
b.WriteString(instructionStyle.Render("↑/↓: Navigate | Enter: Details | Esc: Back | q: Quit"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginInstalledDetail() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
labelStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
if m.selectedInstalledIndex >= len(m.installedPluginsList) {
|
||||
return "No plugin selected"
|
||||
}
|
||||
|
||||
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
||||
|
||||
b.WriteString(titleStyle.Render(plugin.Name))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("ID: "))
|
||||
b.WriteString(normalStyle.Render(plugin.ID))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Category: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Category))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Author: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Author))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Description:"))
|
||||
b.WriteString("\n")
|
||||
wrapped := wrapText(plugin.Description, 60)
|
||||
b.WriteString(normalStyle.Render(wrapped))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Repository: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Repo))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(plugin.Capabilities) > 0 {
|
||||
b.WriteString(labelStyle.Render("Capabilities: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Capabilities, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Compositors) > 0 {
|
||||
b.WriteString(labelStyle.Render("Compositors: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Compositors, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Dependencies) > 0 {
|
||||
b.WriteString(labelStyle.Render("Dependencies: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Dependencies, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if m.installedPluginsError != "" {
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s", m.installedPluginsError)))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("u: Uninstall | Esc: Back | q: Quit"))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func wrapText(text string, width int) string {
|
||||
words := strings.Fields(text)
|
||||
if len(words) == 0 {
|
||||
return text
|
||||
}
|
||||
|
||||
var lines []string
|
||||
currentLine := words[0]
|
||||
|
||||
for _, word := range words[1:] {
|
||||
if len(currentLine)+1+len(word) <= width {
|
||||
currentLine += " " + word
|
||||
} else {
|
||||
lines = append(lines, currentLine)
|
||||
currentLine = word
|
||||
}
|
||||
}
|
||||
lines = append(lines, currentLine)
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
149
core/internal/dms/views_common.go
Normal file
149
core/internal/dms/views_common.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) renderMainMenu() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("dms"))
|
||||
b.WriteString("\n")
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
for i, item := range m.menuItems {
|
||||
if i == m.selectedItem {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("▶ %s", item.Label)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", item.Label)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Enter: Select, q/Esc: Exit"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderShellView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Shell"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Opening interactive shell..."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("This will launch a shell with DMS environment loaded."))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Press any key to launch shell, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderAboutView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("About DankMaterialShell"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf("DMS Management Interface %s", m.version)))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(normalStyle.Render("DankMaterialShell is a comprehensive desktop environment"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("built around Quickshell, providing a modern Material Design"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("experience for Wayland compositors."))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(normalStyle.Render("Components:"))
|
||||
b.WriteString("\n")
|
||||
for _, dep := range m.dependencies {
|
||||
status := "✗"
|
||||
if dep.Status == 1 {
|
||||
status = "✓"
|
||||
}
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s %s", status, dep.Name)))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Esc: Back to main menu"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderBanner() string {
|
||||
theme := tui.TerminalTheme()
|
||||
|
||||
logo := `
|
||||
██████╗ █████╗ ███╗ ██╗██╗ ██╗
|
||||
██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝
|
||||
██║ ██║███████║██╔██╗ ██║█████╔╝
|
||||
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
|
||||
██████╔╝██║ ██║██║ ╚████║██║ ██╗
|
||||
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝`
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Primary)).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
return titleStyle.Render(logo)
|
||||
}
|
||||
529
core/internal/dms/views_features.go
Normal file
529
core/internal/dms/views_features.go
Normal file
@@ -0,0 +1,529 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) renderUpdateView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Update Dependencies"))
|
||||
b.WriteString("\n")
|
||||
|
||||
if len(m.updateDeps) == 0 {
|
||||
b.WriteString("Loading dependencies...\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
categories := m.categorizeDependencies()
|
||||
currentIndex := 0
|
||||
|
||||
for _, category := range []string{"Shell", "Shared Components", "Hyprland Components", "Niri Components"} {
|
||||
deps, exists := categories[category]
|
||||
if !exists || len(deps) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
categoryStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#7060ac")).
|
||||
Bold(true).
|
||||
MarginTop(1)
|
||||
|
||||
b.WriteString(categoryStyle.Render(category + ":"))
|
||||
b.WriteString("\n")
|
||||
|
||||
for _, dep := range deps {
|
||||
var statusText, icon, reinstallMarker string
|
||||
var style lipgloss.Style
|
||||
|
||||
if m.updateToggles[dep.Name] {
|
||||
reinstallMarker = "🔄 "
|
||||
if dep.Status == 0 {
|
||||
statusText = "Will be installed"
|
||||
} else {
|
||||
statusText = "Will be upgraded"
|
||||
}
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500"))
|
||||
} else {
|
||||
switch dep.Status {
|
||||
case 1:
|
||||
icon = "✓"
|
||||
statusText = "Installed"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF"))
|
||||
case 0:
|
||||
icon = "○"
|
||||
statusText = "Not installed"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
case 2:
|
||||
icon = "△"
|
||||
statusText = "Needs update"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500"))
|
||||
case 3:
|
||||
icon = "!"
|
||||
statusText = "Needs reinstall"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500"))
|
||||
}
|
||||
}
|
||||
|
||||
line := fmt.Sprintf("%s%s%-25s %s", reinstallMarker, icon, dep.Name, statusText)
|
||||
|
||||
if currentIndex == m.selectedUpdateDep {
|
||||
line = "▶ " + line
|
||||
selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#7060ac")).Bold(true)
|
||||
b.WriteString(selectedStyle.Render(line))
|
||||
} else {
|
||||
line = " " + line
|
||||
b.WriteString(style.Render(line))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
currentIndex++
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Space: Toggle, Enter: Update Selected, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPasswordView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Sudo Authentication"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Package installation requires sudo privileges."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("Please enter your password to continue:"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
inputStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
maskedPassword := strings.Repeat("*", len(m.passwordInput))
|
||||
b.WriteString(inputStyle.Render("Password: " + maskedPassword))
|
||||
b.WriteString("\n")
|
||||
|
||||
if m.passwordError != "" {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
b.WriteString(errorStyle.Render("✗ " + m.passwordError))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Enter: Continue, Esc: Back, Ctrl+C: Cancel"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderProgressView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Updating Packages"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if !m.updateProgress.complete {
|
||||
progressStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString(progressStyle.Render(m.updateProgress.step))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
progressBar := fmt.Sprintf("[%s%s] %.0f%%",
|
||||
strings.Repeat("█", int(m.updateProgress.progress*30)),
|
||||
strings.Repeat("░", 30-int(m.updateProgress.progress*30)),
|
||||
m.updateProgress.progress*100)
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")).Render(progressBar))
|
||||
b.WriteString("\n")
|
||||
|
||||
if len(m.updateLogs) > 0 {
|
||||
b.WriteString("\n")
|
||||
logHeader := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render("Live Output:")
|
||||
b.WriteString(logHeader)
|
||||
b.WriteString("\n")
|
||||
|
||||
maxLines := 8
|
||||
startIdx := 0
|
||||
if len(m.updateLogs) > maxLines {
|
||||
startIdx = len(m.updateLogs) - maxLines
|
||||
}
|
||||
|
||||
logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
for i := startIdx; i < len(m.updateLogs); i++ {
|
||||
if m.updateLogs[i] != "" {
|
||||
b.WriteString(logStyle.Render(" " + m.updateLogs[i]))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.updateProgress.err != nil {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("✗ Update failed: %v", m.updateProgress.err)))
|
||||
b.WriteString("\n")
|
||||
|
||||
if len(m.updateLogs) > 0 {
|
||||
b.WriteString("\n")
|
||||
logHeader := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render("Error Logs:")
|
||||
b.WriteString(logHeader)
|
||||
b.WriteString("\n")
|
||||
|
||||
maxLines := 15
|
||||
startIdx := 0
|
||||
if len(m.updateLogs) > maxLines {
|
||||
startIdx = len(m.updateLogs) - maxLines
|
||||
}
|
||||
|
||||
logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
for i := startIdx; i < len(m.updateLogs); i++ {
|
||||
if m.updateLogs[i] != "" {
|
||||
b.WriteString(logStyle.Render(" " + m.updateLogs[i]))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to go back"))
|
||||
} else if m.updateProgress.complete {
|
||||
successStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(successStyle.Render("✓ Update complete!"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to return to main menu"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) getFilteredDeps() []DependencyInfo {
|
||||
categories := m.categorizeDependencies()
|
||||
var filtered []DependencyInfo
|
||||
|
||||
for _, category := range []string{"Shell", "Shared Components", "Hyprland Components", "Niri Components"} {
|
||||
deps, exists := categories[category]
|
||||
if exists {
|
||||
filtered = append(filtered, deps...)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (m Model) getDepAtVisualIndex(index int) *DependencyInfo {
|
||||
filtered := m.getFilteredDeps()
|
||||
if index >= 0 && index < len(filtered) {
|
||||
return &filtered[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterPasswordView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Sudo Authentication"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Greeter installation requires sudo privileges."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("Please enter your password to continue:"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
inputStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
maskedPassword := strings.Repeat("*", len(m.greeterPasswordInput))
|
||||
b.WriteString(inputStyle.Render("Password: " + maskedPassword))
|
||||
b.WriteString("\n")
|
||||
|
||||
if m.greeterPasswordError != "" {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
b.WriteString(errorStyle.Render("✗ " + m.greeterPasswordError))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Enter: Continue, Esc: Back, Ctrl+C: Cancel"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterCompositorSelect() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Select Compositor"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Multiple compositors detected. Choose which one to use for the greeter:"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
for i, comp := range m.greeterCompositors {
|
||||
if i == m.greeterSelectedComp {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("▶ %s", comp)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", comp)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Enter: Select, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterMenu() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Greeter Management"))
|
||||
b.WriteString("\n")
|
||||
|
||||
greeterMenuItems := []string{"Install Greeter"}
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
for i, item := range greeterMenuItems {
|
||||
if i == m.selectedGreeterItem {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("▶ %s", item)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", item)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Enter: Select, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterInstalling() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Installing Greeter"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if !m.greeterProgress.complete {
|
||||
progressStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString(progressStyle.Render(m.greeterProgress.step))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(m.greeterLogs) > 0 {
|
||||
b.WriteString("\n")
|
||||
logHeader := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render("Output:")
|
||||
b.WriteString(logHeader)
|
||||
b.WriteString("\n")
|
||||
|
||||
maxLines := 10
|
||||
startIdx := 0
|
||||
if len(m.greeterLogs) > maxLines {
|
||||
startIdx = len(m.greeterLogs) - maxLines
|
||||
}
|
||||
|
||||
logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
for i := startIdx; i < len(m.greeterLogs); i++ {
|
||||
if m.greeterLogs[i] != "" {
|
||||
b.WriteString(logStyle.Render(" " + m.greeterLogs[i]))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.greeterProgress.err != nil {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("✗ Installation failed: %v", m.greeterProgress.err)))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to go back"))
|
||||
} else if m.greeterProgress.complete {
|
||||
successStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(successStyle.Render("✓ Greeter installation complete!"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("To test the greeter, run:"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render(" sudo systemctl start greetd"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(normalStyle.Render("To enable on boot, run:"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render(" sudo systemctl enable --now greetd"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to return to main menu"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) categorizeDependencies() map[string][]DependencyInfo {
|
||||
categories := map[string][]DependencyInfo{
|
||||
"Shell": {},
|
||||
"Shared Components": {},
|
||||
"Hyprland Components": {},
|
||||
"Niri Components": {},
|
||||
}
|
||||
|
||||
excludeList := map[string]bool{
|
||||
"git": true,
|
||||
"polkit-agent": true,
|
||||
"jq": true,
|
||||
"xdg-desktop-portal": true,
|
||||
"xdg-desktop-portal-wlr": true,
|
||||
"xdg-desktop-portal-hyprland": true,
|
||||
"xdg-desktop-portal-gtk": true,
|
||||
}
|
||||
|
||||
for _, dep := range m.updateDeps {
|
||||
if excludeList[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
switch dep.Name {
|
||||
case "dms (DankMaterialShell)", "quickshell":
|
||||
categories["Shell"] = append(categories["Shell"], dep)
|
||||
case "hyprland", "grim", "slurp", "hyprctl", "grimblast":
|
||||
categories["Hyprland Components"] = append(categories["Hyprland Components"], dep)
|
||||
case "niri":
|
||||
categories["Niri Components"] = append(categories["Niri Components"], dep)
|
||||
case "kitty", "alacritty", "ghostty", "hyprpicker":
|
||||
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
||||
default:
|
||||
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
||||
}
|
||||
}
|
||||
|
||||
return categories
|
||||
}
|
||||
Reference in New Issue
Block a user