mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 13:32:50 -05:00
core: ipc fill in help, remove management tui
This commit is contained in:
@@ -64,9 +64,8 @@ var killCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ipcCmd = &cobra.Command{
|
var ipcCmd = &cobra.Command{
|
||||||
Use: "ipc",
|
Use: "ipc [target] [function] [args...]",
|
||||||
Short: "Send IPC commands to running DMS shell",
|
Short: "Send IPC commands to running DMS shell",
|
||||||
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
|
|
||||||
PreRunE: findConfig,
|
PreRunE: findConfig,
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
_ = findConfig(cmd, args)
|
_ = findConfig(cmd, args)
|
||||||
@@ -77,6 +76,13 @@ var ipcCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||||
|
_ = findConfig(cmd, args)
|
||||||
|
printIPCHelp()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var debugSrvCmd = &cobra.Command{
|
var debugSrvCmd = &cobra.Command{
|
||||||
Use: "debug-srv",
|
Use: "debug-srv",
|
||||||
Short: "Start the debug server",
|
Short: "Start the debug server",
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/dms"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,11 +18,9 @@ var rootCmd = &cobra.Command{
|
|||||||
Use: "dms",
|
Use: "dms",
|
||||||
Short: "dms CLI",
|
Short: "dms CLI",
|
||||||
Long: "dms is the DankMaterialShell management CLI and backend server.",
|
Long: "dms is the DankMaterialShell management CLI and backend server.",
|
||||||
Run: runInteractiveMode,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Add the -c flag
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
|
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +34,7 @@ func findConfig(cmd *cobra.Command, args []string) error {
|
|||||||
if statErr == nil && !info.IsDir() {
|
if statErr == nil && !info.IsDir() {
|
||||||
configPath = customConfigPath
|
configPath = customConfigPath
|
||||||
log.Debug("Using config from: %s", configPath)
|
log.Debug("Using config from: %s", configPath)
|
||||||
return nil // <-- Guard statement
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if statErr != nil {
|
if statErr != nil {
|
||||||
@@ -76,18 +72,3 @@ func findConfig(cmd *cobra.Command, args []string) error {
|
|||||||
log.Debug("Using config from: %s", configPath)
|
log.Debug("Using config from: %s", configPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func runInteractiveMode(cmd *cobra.Command, args []string) {
|
|
||||||
detector, _ := dms.NewDetector()
|
|
||||||
|
|
||||||
if !detector.IsDMSInstalled() {
|
|
||||||
log.Error("DankMaterialShell (DMS) is not detected as installed on this system.")
|
|
||||||
log.Info("Please install DMS using dankinstall before using this management interface.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
model := dms.NewModel(Version)
|
|
||||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
|
||||||
if _, err := p.Run(); err != nil {
|
|
||||||
log.Fatalf("Error running program: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -618,9 +618,8 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
|||||||
|
|
||||||
func runShellIPCCommand(args []string) {
|
func runShellIPCCommand(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
log.Error("IPC command requires arguments")
|
printIPCHelp()
|
||||||
log.Info("Usage: dms ipc <command> [args...]")
|
return
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if args[0] != "call" {
|
if args[0] != "call" {
|
||||||
@@ -642,3 +641,45 @@ func runShellIPCCommand(args []string) {
|
|||||||
log.Fatalf("Error running IPC command: %v", err)
|
log.Fatalf("Error running IPC command: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printIPCHelp() {
|
||||||
|
fmt.Println("Usage: dms ipc <target> <function> [args...]")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
cmdArgs := []string{"ipc"}
|
||||||
|
if qsHasAnyDisplay() {
|
||||||
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "-p", configPath, "show")
|
||||||
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Could not retrieve available IPC targets (is DMS running?)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targets := parseTargetsFromIPCShowOutput(string(output))
|
||||||
|
if len(targets) == 0 {
|
||||||
|
fmt.Println("No IPC targets available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Targets:")
|
||||||
|
|
||||||
|
targetNames := make([]string, 0, len(targets))
|
||||||
|
for name := range targets {
|
||||||
|
targetNames = append(targetNames, name)
|
||||||
|
}
|
||||||
|
slices.Sort(targetNames)
|
||||||
|
|
||||||
|
for _, targetName := range targetNames {
|
||||||
|
funcs := targets[targetName]
|
||||||
|
funcNames := make([]string, 0, len(funcs))
|
||||||
|
for fn := range funcs {
|
||||||
|
funcNames = append(funcNames, fn)
|
||||||
|
}
|
||||||
|
slices.Sort(funcNames)
|
||||||
|
fmt.Printf(" %-16s %s\n", targetName, strings.Join(funcNames, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,450 +0,0 @@
|
|||||||
//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()
|
|
||||||
var dependencies []DependencyInfo
|
|
||||||
var hyprlandInstalled, niriInstalled bool
|
|
||||||
var err error
|
|
||||||
if detector != nil {
|
|
||||||
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 pluginUpdatedMsg:
|
|
||||||
if msg.err != nil {
|
|
||||||
m.installedPluginsError = msg.err.Error()
|
|
||||||
} else {
|
|
||||||
m.installedPluginsError = ""
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
//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()
|
|
||||||
|
|
||||||
var dependencies []DependencyInfo
|
|
||||||
var hyprlandInstalled, niriInstalled bool
|
|
||||||
|
|
||||||
if detector != nil {
|
|
||||||
dependencies = detector.GetInstalledComponents()
|
|
||||||
hyprlandInstalled, niriInstalled, _ = detector.GetWindowManagerStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
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 pluginUpdatedMsg:
|
|
||||||
if msg.err != nil {
|
|
||||||
m.installedPluginsError = msg.err.Error()
|
|
||||||
} else {
|
|
||||||
m.installedPluginsError = ""
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
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{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var components []DependencyInfo
|
|
||||||
for _, dep := range dependencies {
|
|
||||||
components = append(components, DependencyInfo{
|
|
||||||
Name: dep.Name,
|
|
||||||
Status: dep.Status,
|
|
||||||
Description: dep.Description,
|
|
||||||
Required: dep.Required,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return components
|
|
||||||
}
|
|
||||||
|
|
||||||
type DependencyInfo struct {
|
|
||||||
Name string
|
|
||||||
Status deps.DependencyStatus
|
|
||||||
Description string
|
|
||||||
Required bool
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@@ -1,392 +0,0 @@
|
|||||||
//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
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
//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
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
//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
|
|
||||||
}
|
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
case "p":
|
|
||||||
if m.selectedInstalledIndex < len(m.installedPluginsList) {
|
|
||||||
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
|
||||||
return m, updatePlugin(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
|
|
||||||
}
|
|
||||||
|
|
||||||
type pluginUpdatedMsg 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}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updatePlugin(plugin pluginInfo) tea.Cmd {
|
|
||||||
return func() tea.Msg {
|
|
||||||
manager, err := plugins.NewManager()
|
|
||||||
if err != nil {
|
|
||||||
return pluginUpdatedMsg{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.Update(p); err != nil {
|
|
||||||
return pluginUpdatedMsg{pluginName: plugin.Name, err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pluginUpdatedMsg{pluginName: plugin.Name}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
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")
|
|
||||||
if len(m.dependencies) == 0 {
|
|
||||||
b.WriteString(normalStyle.Render("\n Component detection not supported on this platform."))
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,529 +0,0 @@
|
|||||||
//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", "hyprctl":
|
|
||||||
categories["Hyprland Components"] = append(categories["Hyprland Components"], dep)
|
|
||||||
case "niri":
|
|
||||||
categories["Niri Components"] = append(categories["Niri Components"], dep)
|
|
||||||
case "kitty", "alacritty", "ghostty":
|
|
||||||
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