mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
Compare commits
8 Commits
d08496f237
...
15dc91f779
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15dc91f779 | ||
|
|
dd3d2908a2 | ||
|
|
0857023dba | ||
|
|
1edc8f468e | ||
|
|
2681fe87bb | ||
|
|
3f0d0f4d95 | ||
|
|
f24ecf1b99 | ||
|
|
acdd1d2ec4 |
@@ -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
|
|
||||||
}
|
|
||||||
@@ -83,6 +83,7 @@ Singleton {
|
|||||||
property string nightModeLocationProvider: ""
|
property string nightModeLocationProvider: ""
|
||||||
|
|
||||||
property var pinnedApps: []
|
property var pinnedApps: []
|
||||||
|
property int dockLauncherPosition: 0
|
||||||
property var hiddenTrayIds: []
|
property var hiddenTrayIds: []
|
||||||
property var recentColors: []
|
property var recentColors: []
|
||||||
property bool showThirdPartyPlugins: false
|
property bool showThirdPartyPlugins: false
|
||||||
@@ -757,6 +758,11 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setDockLauncherPosition(position) {
|
||||||
|
dockLauncherPosition = position;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
function addPinnedApp(appId) {
|
function addPinnedApp(appId) {
|
||||||
if (!appId)
|
if (!appId)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ Singleton {
|
|||||||
property bool use24HourClock: true
|
property bool use24HourClock: true
|
||||||
property bool showSeconds: false
|
property bool showSeconds: false
|
||||||
property bool useFahrenheit: false
|
property bool useFahrenheit: false
|
||||||
|
property string windSpeedUnit: "kmh"
|
||||||
property bool nightModeEnabled: false
|
property bool nightModeEnabled: false
|
||||||
property int animationSpeed: SettingsData.AnimationSpeed.Short
|
property int animationSpeed: SettingsData.AnimationSpeed.Short
|
||||||
property int customAnimationDuration: 500
|
property int customAnimationDuration: 500
|
||||||
@@ -430,6 +431,13 @@ Singleton {
|
|||||||
property real dockBorderOpacity: 1.0
|
property real dockBorderOpacity: 1.0
|
||||||
property int dockBorderThickness: 1
|
property int dockBorderThickness: 1
|
||||||
property bool dockIsolateDisplays: false
|
property bool dockIsolateDisplays: false
|
||||||
|
property bool dockLauncherEnabled: false
|
||||||
|
property string dockLauncherLogoMode: "apps"
|
||||||
|
property string dockLauncherLogoCustomPath: ""
|
||||||
|
property string dockLauncherLogoColorOverride: ""
|
||||||
|
property int dockLauncherLogoSizeOffset: 0
|
||||||
|
property real dockLauncherLogoBrightness: 0.5
|
||||||
|
property real dockLauncherLogoContrast: 1
|
||||||
|
|
||||||
property bool notificationOverlayEnabled: false
|
property bool notificationOverlayEnabled: false
|
||||||
property int overviewRows: 2
|
property int overviewRows: 2
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ var SPEC = {
|
|||||||
weatherCoordinates: { def: "40.7128,-74.0060" },
|
weatherCoordinates: { def: "40.7128,-74.0060" },
|
||||||
|
|
||||||
pinnedApps: { def: [] },
|
pinnedApps: { def: [] },
|
||||||
|
dockLauncherPosition: { def: 0 },
|
||||||
hiddenTrayIds: { def: [] },
|
hiddenTrayIds: { def: [] },
|
||||||
recentColors: { def: [] },
|
recentColors: { def: [] },
|
||||||
showThirdPartyPlugins: { def: false },
|
showThirdPartyPlugins: { def: false },
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ var SPEC = {
|
|||||||
use24HourClock: { def: true },
|
use24HourClock: { def: true },
|
||||||
showSeconds: { def: false },
|
showSeconds: { def: false },
|
||||||
useFahrenheit: { def: false },
|
useFahrenheit: { def: false },
|
||||||
|
windSpeedUnit: { def: "kmh" },
|
||||||
nightModeEnabled: { def: false },
|
nightModeEnabled: { def: false },
|
||||||
animationSpeed: { def: 1 },
|
animationSpeed: { def: 1 },
|
||||||
customAnimationDuration: { def: 500 },
|
customAnimationDuration: { def: 500 },
|
||||||
@@ -254,6 +255,13 @@ var SPEC = {
|
|||||||
dockBorderOpacity: { def: 1.0, coerce: percentToUnit },
|
dockBorderOpacity: { def: 1.0, coerce: percentToUnit },
|
||||||
dockBorderThickness: { def: 1 },
|
dockBorderThickness: { def: 1 },
|
||||||
dockIsolateDisplays: { def: false },
|
dockIsolateDisplays: { def: false },
|
||||||
|
dockLauncherEnabled: { def: false },
|
||||||
|
dockLauncherLogoMode: { def: "apps" },
|
||||||
|
dockLauncherLogoCustomPath: { def: "" },
|
||||||
|
dockLauncherLogoColorOverride: { def: "" },
|
||||||
|
dockLauncherLogoSizeOffset: { def: 0 },
|
||||||
|
dockLauncherLogoBrightness: { def: 0.5, coerce: percentToUnit },
|
||||||
|
dockLauncherLogoContrast: { def: 1, coerce: percentToUnit },
|
||||||
|
|
||||||
notificationOverlayEnabled: { def: false },
|
notificationOverlayEnabled: { def: false },
|
||||||
overviewRows: { def: 2, persist: false },
|
overviewRows: { def: 2, persist: false },
|
||||||
|
|||||||
@@ -76,8 +76,6 @@ FocusScope {
|
|||||||
function showContextMenu(item, x, y, fromKeyboard) {
|
function showContextMenu(item, x, y, fromKeyboard) {
|
||||||
if (!item)
|
if (!item)
|
||||||
return;
|
return;
|
||||||
if (item.isCore)
|
|
||||||
return;
|
|
||||||
if (!contextMenu.hasContextMenuActions(item))
|
if (!contextMenu.hasContextMenuActions(item))
|
||||||
return;
|
return;
|
||||||
contextMenu.show(x, y, item, fromKeyboard);
|
contextMenu.show(x, y, item, fromKeyboard);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Popup {
|
|||||||
function hasContextMenuActions(spotlightItem) {
|
function hasContextMenuActions(spotlightItem) {
|
||||||
if (!spotlightItem)
|
if (!spotlightItem)
|
||||||
return false;
|
return false;
|
||||||
if (spotlightItem.type === "app" && !spotlightItem.isCore)
|
if (spotlightItem.type === "app")
|
||||||
return true;
|
return true;
|
||||||
if (spotlightItem.type === "plugin" && spotlightItem.pluginId) {
|
if (spotlightItem.type === "plugin" && spotlightItem.pluginId) {
|
||||||
var instance = PluginService.pluginInstances[spotlightItem.pluginId];
|
var instance = PluginService.pluginInstances[spotlightItem.pluginId];
|
||||||
@@ -34,9 +34,16 @@ Popup {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property var desktopEntry: item?.data ?? null
|
readonly property bool isCoreApp: item?.type === "app" && item?.isCore
|
||||||
readonly property string appId: desktopEntry?.id || desktopEntry?.execString || ""
|
readonly property var coreAppData: isCoreApp ? item?.data ?? null : null
|
||||||
readonly property bool isPinned: SessionData.isPinnedApp(appId)
|
readonly property var desktopEntry: !isCoreApp ? (item?.data ?? null) : null
|
||||||
|
readonly property string appId: {
|
||||||
|
if (isCoreApp) {
|
||||||
|
return item?.id || coreAppData?.builtInPluginId || "";
|
||||||
|
}
|
||||||
|
return desktopEntry?.id || desktopEntry?.execString || "";
|
||||||
|
}
|
||||||
|
readonly property bool isPinned: appId ? SessionData.isPinnedApp(appId) : false
|
||||||
readonly property bool isRegularApp: item?.type === "app" && !item.isCore && desktopEntry
|
readonly property bool isRegularApp: item?.type === "app" && !item.isCore && desktopEntry
|
||||||
readonly property bool isPluginItem: item?.type === "plugin"
|
readonly property bool isPluginItem: item?.type === "plugin"
|
||||||
|
|
||||||
@@ -82,15 +89,14 @@ Popup {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!desktopEntry)
|
if (item?.type === "app") {
|
||||||
return items;
|
items.push({
|
||||||
|
type: "item",
|
||||||
items.push({
|
icon: isPinned ? "keep_off" : "push_pin",
|
||||||
type: "item",
|
text: isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock"),
|
||||||
icon: isPinned ? "keep_off" : "push_pin",
|
action: togglePin
|
||||||
text: isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock"),
|
});
|
||||||
action: togglePin
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (isRegularApp) {
|
if (isRegularApp) {
|
||||||
items.push({
|
items.push({
|
||||||
@@ -132,18 +138,6 @@ Popup {
|
|||||||
action: launchApp
|
action: launchApp
|
||||||
});
|
});
|
||||||
|
|
||||||
if (SessionService.nvidiaCommand) {
|
|
||||||
items.push({
|
|
||||||
type: "separator"
|
|
||||||
});
|
|
||||||
items.push({
|
|
||||||
type: "item",
|
|
||||||
icon: "memory",
|
|
||||||
text: I18n.tr("Launch on dGPU"),
|
|
||||||
action: launchWithNvidia
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +206,14 @@ Popup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function launchApp() {
|
function launchApp() {
|
||||||
|
if (isCoreApp) {
|
||||||
|
if (!coreAppData)
|
||||||
|
return;
|
||||||
|
AppSearchService.executeCoreApp(coreAppData);
|
||||||
|
controller?.itemExecuted();
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!desktopEntry)
|
if (!desktopEntry)
|
||||||
return;
|
return;
|
||||||
SessionService.launchDesktopEntry(desktopEntry);
|
SessionService.launchDesktopEntry(desktopEntry);
|
||||||
|
|||||||
@@ -47,10 +47,17 @@ Rectangle {
|
|||||||
readonly property var humidity: WeatherService.formatPercent(root.forecastData?.humidity)
|
readonly property var humidity: WeatherService.formatPercent(root.forecastData?.humidity)
|
||||||
readonly property string humidityText: humidity ?? "--"
|
readonly property string humidityText: humidity ?? "--"
|
||||||
|
|
||||||
readonly property var wind: WeatherService.formatSpeed(root.forecastData?.wind)
|
readonly property var wind: {
|
||||||
|
SettingsData.windSpeedUnit;
|
||||||
|
SettingsData.useFahrenheit;
|
||||||
|
return WeatherService.formatSpeed(root.forecastData?.wind);
|
||||||
|
}
|
||||||
readonly property string windText: wind ?? "--"
|
readonly property string windText: wind ?? "--"
|
||||||
|
|
||||||
readonly property var pressure: WeatherService.formatPressure(root.forecastData?.pressure)
|
readonly property var pressure: {
|
||||||
|
SettingsData.useFahrenheit;
|
||||||
|
return WeatherService.formatPressure(root.forecastData?.pressure);
|
||||||
|
}
|
||||||
readonly property string pressureText: pressure ?? "--"
|
readonly property string pressureText: pressure ?? "--"
|
||||||
|
|
||||||
readonly property var precipitation: root.forecastData?.precipitationProbability
|
readonly property var precipitation: root.forecastData?.precipitationProbability
|
||||||
|
|||||||
@@ -229,10 +229,17 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
property var feelsLike: SettingsData.useFahrenheit ? (WeatherService.weather.feelsLikeF || WeatherService.weather.tempF) : (WeatherService.weather.feelsLike || WeatherService.weather.temp)
|
||||||
|
text: I18n.tr("Feels Like %1°", "weather feels like temperature").arg(feelsLike)
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.withAlpha(Theme.surfaceText, 0.7)
|
||||||
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: WeatherService.weather.city || ""
|
text: WeatherService.weather.city || ""
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
color: Theme.withAlpha(Theme.surfaceText, 0.7)
|
||||||
visible: text.length > 0
|
visible: text.length > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,7 +260,7 @@ Item {
|
|||||||
id: sunriseIcon
|
id: sunriseIcon
|
||||||
name: "wb_twilight"
|
name: "wb_twilight"
|
||||||
size: Theme.iconSize - 6
|
size: Theme.iconSize - 6
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
color: Theme.withAlpha(Theme.surfaceText, 0.6)
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
@@ -272,7 +279,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
color: Theme.withAlpha(Theme.surfaceText, 0.6)
|
||||||
anchors.left: sunriseIcon.right
|
anchors.left: sunriseIcon.right
|
||||||
anchors.leftMargin: Theme.spacingXS
|
anchors.leftMargin: Theme.spacingXS
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|||||||
@@ -29,12 +29,29 @@ Item {
|
|||||||
property bool showTooltip: mouseArea.containsMouse && !dragging
|
property bool showTooltip: mouseArea.containsMouse && !dragging
|
||||||
property var cachedDesktopEntry: null
|
property var cachedDesktopEntry: null
|
||||||
property real actualIconSize: 40
|
property real actualIconSize: 40
|
||||||
|
readonly property string coreIconColorOverride: SettingsData.dockLauncherLogoColorOverride
|
||||||
|
readonly property bool coreIconHasCustomColor: coreIconColorOverride !== "" && coreIconColorOverride !== "primary" && coreIconColorOverride !== "surface"
|
||||||
|
readonly property color effectiveCoreIconColor: {
|
||||||
|
if (coreIconColorOverride === "primary")
|
||||||
|
return Theme.primary;
|
||||||
|
if (coreIconColorOverride === "surface")
|
||||||
|
return Theme.surfaceText;
|
||||||
|
if (coreIconColorOverride !== "")
|
||||||
|
return coreIconColorOverride;
|
||||||
|
return Theme.surfaceText;
|
||||||
|
}
|
||||||
|
readonly property real effectiveCoreIconBrightness: coreIconHasCustomColor ? SettingsData.dockLauncherLogoBrightness : 0.0
|
||||||
|
readonly property real effectiveCoreIconContrast: coreIconHasCustomColor ? SettingsData.dockLauncherLogoContrast : 0.0
|
||||||
|
|
||||||
function updateDesktopEntry() {
|
function updateDesktopEntry() {
|
||||||
if (!appData || appData.appId === "__SEPARATOR__") {
|
if (!appData || appData.appId === "__SEPARATOR__") {
|
||||||
cachedDesktopEntry = null;
|
cachedDesktopEntry = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (appData.isCoreApp) {
|
||||||
|
cachedDesktopEntry = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
const moddedId = Paths.moddedAppId(appData.appId);
|
const moddedId = Paths.moddedAppId(appData.appId);
|
||||||
cachedDesktopEntry = DesktopEntries.heuristicLookup(moddedId);
|
cachedDesktopEntry = DesktopEntries.heuristicLookup(moddedId);
|
||||||
}
|
}
|
||||||
@@ -85,7 +102,12 @@ Item {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const appName = Paths.getAppName(appData.appId, cachedDesktopEntry);
|
let appName;
|
||||||
|
if (appData.isCoreApp && appData.coreAppData) {
|
||||||
|
appName = appData.coreAppData.name || appData.appId;
|
||||||
|
} else {
|
||||||
|
appName = Paths.getAppName(appData.appId, cachedDesktopEntry);
|
||||||
|
}
|
||||||
|
|
||||||
if ((appData.type === "window" && showWindowTitle) || (appData.type === "grouped" && appData.windowTitle)) {
|
if ((appData.type === "window" && showWindowTitle) || (appData.type === "grouped" && appData.windowTitle)) {
|
||||||
const title = appData.type === "window" ? windowTitle : appData.windowTitle;
|
const title = appData.type === "window" ? windowTitle : appData.windowTitle;
|
||||||
@@ -208,7 +230,7 @@ Item {
|
|||||||
targetIndex = -1;
|
targetIndex = -1;
|
||||||
originalIndex = -1;
|
originalIndex = -1;
|
||||||
|
|
||||||
if (dockApps && !didReorder) {
|
if (dockApps) {
|
||||||
dockApps.draggedIndex = -1;
|
dockApps.draggedIndex = -1;
|
||||||
dockApps.dropTargetIndex = -1;
|
dockApps.dropTargetIndex = -1;
|
||||||
}
|
}
|
||||||
@@ -227,6 +249,10 @@ Item {
|
|||||||
case "pinned":
|
case "pinned":
|
||||||
if (!appData.appId)
|
if (!appData.appId)
|
||||||
return;
|
return;
|
||||||
|
if (appData.isCoreApp && appData.coreAppData) {
|
||||||
|
AppSearchService.executeCoreApp(appData.coreAppData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pinnedEntry = cachedDesktopEntry;
|
const pinnedEntry = cachedDesktopEntry;
|
||||||
if (pinnedEntry) {
|
if (pinnedEntry) {
|
||||||
AppUsageHistoryData.addAppUsage({
|
AppUsageHistoryData.addAppUsage({
|
||||||
@@ -248,6 +274,10 @@ Item {
|
|||||||
if (appData.windowCount === 0) {
|
if (appData.windowCount === 0) {
|
||||||
if (!appData.appId)
|
if (!appData.appId)
|
||||||
return;
|
return;
|
||||||
|
if (appData.isCoreApp && appData.coreAppData) {
|
||||||
|
AppSearchService.executeCoreApp(appData.coreAppData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const groupedEntry = cachedDesktopEntry;
|
const groupedEntry = cachedDesktopEntry;
|
||||||
if (groupedEntry) {
|
if (groupedEntry) {
|
||||||
AppUsageHistoryData.addAppUsage({
|
AppUsageHistoryData.addAppUsage({
|
||||||
@@ -374,6 +404,19 @@ Item {
|
|||||||
z: -1
|
z: -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppIconRenderer {
|
||||||
|
id: coreIcon
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
iconSize: actualIconSize
|
||||||
|
iconValue: appData && appData.isCoreApp && appData.coreAppData ? (appData.coreAppData.icon || "") : ""
|
||||||
|
colorOverride: effectiveCoreIconColor
|
||||||
|
brightnessOverride: effectiveCoreIconBrightness
|
||||||
|
contrastOverride: effectiveCoreIconContrast
|
||||||
|
fallbackText: "?"
|
||||||
|
visible: iconValue !== ""
|
||||||
|
}
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
id: iconImg
|
id: iconImg
|
||||||
|
|
||||||
@@ -383,12 +426,15 @@ Item {
|
|||||||
if (!appData || appData.appId === "__SEPARATOR__") {
|
if (!appData || appData.appId === "__SEPARATOR__") {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
if (appData.isCoreApp && appData.coreAppData) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
return Paths.getAppIcon(appData.appId, cachedDesktopEntry);
|
return Paths.getAppIcon(appData.appId, cachedDesktopEntry);
|
||||||
}
|
}
|
||||||
mipmap: true
|
mipmap: true
|
||||||
smooth: true
|
smooth: true
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
visible: status === Image.Ready
|
visible: status === Image.Ready && !coreIcon.visible
|
||||||
layer.enabled: appData && appData.appId === "org.quickshell"
|
layer.enabled: appData && appData.appId === "org.quickshell"
|
||||||
layer.smooth: true
|
layer.smooth: true
|
||||||
layer.mipmap: true
|
layer.mipmap: true
|
||||||
@@ -403,7 +449,7 @@ Item {
|
|||||||
width: actualIconSize
|
width: actualIconSize
|
||||||
height: actualIconSize
|
height: actualIconSize
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: iconImg.status !== Image.Ready && appData && appData.appId && !Paths.isSteamApp(appData.appId)
|
visible: !coreIcon.visible && iconImg.status !== Image.Ready && appData && appData.appId && !Paths.isSteamApp(appData.appId)
|
||||||
color: Theme.surfaceLight
|
color: Theme.surfaceLight
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.width: 1
|
border.width: 1
|
||||||
@@ -416,7 +462,12 @@ Item {
|
|||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
const appName = Paths.getAppName(appData.appId, cachedDesktopEntry);
|
let appName;
|
||||||
|
if (appData.isCoreApp && appData.coreAppData) {
|
||||||
|
appName = appData.coreAppData.name || appData.appId;
|
||||||
|
} else {
|
||||||
|
appName = Paths.getAppName(appData.appId, cachedDesktopEntry);
|
||||||
|
}
|
||||||
return appName.charAt(0).toUpperCase();
|
return appName.charAt(0).toUpperCase();
|
||||||
}
|
}
|
||||||
font.pixelSize: Math.max(8, parent.width * 0.35)
|
font.pixelSize: Math.max(8, parent.width * 0.35)
|
||||||
@@ -430,7 +481,7 @@ Item {
|
|||||||
size: actualIconSize
|
size: actualIconSize
|
||||||
name: "sports_esports"
|
name: "sports_esports"
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
visible: iconImg.status !== Image.Ready && appData && appData.appId && Paths.isSteamApp(appData.appId)
|
visible: !coreIcon.visible && iconImg.status !== Image.Ready && appData && appData.appId && Paths.isSteamApp(appData.appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
|
|||||||
@@ -22,18 +22,34 @@ Item {
|
|||||||
implicitWidth: isVertical ? appLayout.height : appLayout.width
|
implicitWidth: isVertical ? appLayout.height : appLayout.width
|
||||||
implicitHeight: isVertical ? appLayout.width : appLayout.height
|
implicitHeight: isVertical ? appLayout.width : appLayout.height
|
||||||
|
|
||||||
function movePinnedApp(fromIndex, toIndex) {
|
function dockIndexToPinnedIndex(dockIndex) {
|
||||||
if (fromIndex === toIndex) {
|
if (!SettingsData.dockLauncherEnabled) {
|
||||||
|
return dockIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const launcherPos = SessionData.dockLauncherPosition;
|
||||||
|
if (dockIndex < launcherPos) {
|
||||||
|
return dockIndex;
|
||||||
|
} else {
|
||||||
|
return dockIndex - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function movePinnedApp(fromDockIndex, toDockIndex) {
|
||||||
|
const fromPinnedIndex = dockIndexToPinnedIndex(fromDockIndex);
|
||||||
|
const toPinnedIndex = dockIndexToPinnedIndex(toDockIndex);
|
||||||
|
|
||||||
|
if (fromPinnedIndex === toPinnedIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPinned = [...(SessionData.pinnedApps || [])];
|
const currentPinned = [...(SessionData.pinnedApps || [])];
|
||||||
if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 || toIndex >= currentPinned.length) {
|
if (fromPinnedIndex < 0 || fromPinnedIndex >= currentPinned.length || toPinnedIndex < 0 || toPinnedIndex >= currentPinned.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const movedApp = currentPinned.splice(fromIndex, 1)[0];
|
const movedApp = currentPinned.splice(fromPinnedIndex, 1)[0];
|
||||||
currentPinned.splice(toIndex, 0, movedApp);
|
currentPinned.splice(toPinnedIndex, 0, movedApp);
|
||||||
|
|
||||||
SessionData.setPinnedApps(currentPinned);
|
SessionData.setPinnedApps(currentPinned);
|
||||||
}
|
}
|
||||||
@@ -75,6 +91,51 @@ Item {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCoreAppData(appId) {
|
||||||
|
if (typeof AppSearchService === "undefined")
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const coreApps = AppSearchService.coreApps || [];
|
||||||
|
for (let i = 0; i < coreApps.length; i++) {
|
||||||
|
const app = coreApps[i];
|
||||||
|
if (app.builtInPluginId === appId) {
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCoreAppDataByTitle(windowTitle) {
|
||||||
|
if (typeof AppSearchService === "undefined" || !windowTitle)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const coreApps = AppSearchService.coreApps || [];
|
||||||
|
for (let i = 0; i < coreApps.length; i++) {
|
||||||
|
const app = coreApps[i];
|
||||||
|
if (app.name === windowTitle) {
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertLauncher(targetArray) {
|
||||||
|
if (!SettingsData.dockLauncherEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const launcherItem = {
|
||||||
|
uniqueKey: "launcher_button",
|
||||||
|
type: "launcher",
|
||||||
|
appId: "__LAUNCHER__",
|
||||||
|
toplevel: null,
|
||||||
|
isPinned: true,
|
||||||
|
isRunning: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const pos = Math.max(0, Math.min(SessionData.dockLauncherPosition, targetArray.length));
|
||||||
|
targetArray.splice(pos, 0, launcherItem);
|
||||||
|
}
|
||||||
|
|
||||||
function updateModel() {
|
function updateModel() {
|
||||||
const items = [];
|
const items = [];
|
||||||
const pinnedApps = [...(SessionData.pinnedApps || [])];
|
const pinnedApps = [...(SessionData.pinnedApps || [])];
|
||||||
@@ -86,21 +147,35 @@ Item {
|
|||||||
|
|
||||||
pinnedApps.forEach(rawAppId => {
|
pinnedApps.forEach(rawAppId => {
|
||||||
const appId = Paths.moddedAppId(rawAppId);
|
const appId = Paths.moddedAppId(rawAppId);
|
||||||
|
const coreAppData = getCoreAppData(appId);
|
||||||
appGroups.set(appId, {
|
appGroups.set(appId, {
|
||||||
appId: appId,
|
appId: appId,
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
windows: []
|
windows: [],
|
||||||
|
isCoreApp: coreAppData !== null,
|
||||||
|
coreAppData: coreAppData
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
sortedToplevels.forEach((toplevel, index) => {
|
sortedToplevels.forEach((toplevel, index) => {
|
||||||
const rawAppId = toplevel.appId || "unknown";
|
const rawAppId = toplevel.appId || "unknown";
|
||||||
const appId = Paths.moddedAppId(rawAppId);
|
let appId = Paths.moddedAppId(rawAppId);
|
||||||
|
|
||||||
|
let coreAppData = null;
|
||||||
|
if (rawAppId === "org.quickshell") {
|
||||||
|
coreAppData = getCoreAppDataByTitle(toplevel.title);
|
||||||
|
if (coreAppData) {
|
||||||
|
appId = coreAppData.builtInPluginId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!appGroups.has(appId)) {
|
if (!appGroups.has(appId)) {
|
||||||
appGroups.set(appId, {
|
appGroups.set(appId, {
|
||||||
appId: appId,
|
appId: appId,
|
||||||
isPinned: false,
|
isPinned: false,
|
||||||
windows: []
|
windows: [],
|
||||||
|
isCoreApp: coreAppData !== null,
|
||||||
|
coreAppData: coreAppData
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +199,9 @@ Item {
|
|||||||
isPinned: group.isPinned,
|
isPinned: group.isPinned,
|
||||||
isRunning: group.windows.length > 0,
|
isRunning: group.windows.length > 0,
|
||||||
windowCount: group.windows.length,
|
windowCount: group.windows.length,
|
||||||
allWindows: group.windows
|
allWindows: group.windows,
|
||||||
|
isCoreApp: group.isCoreApp || false,
|
||||||
|
coreAppData: group.coreAppData || null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (group.isPinned) {
|
if (group.isPinned) {
|
||||||
@@ -136,6 +213,8 @@ Item {
|
|||||||
|
|
||||||
pinnedGroups.forEach(item => items.push(item));
|
pinnedGroups.forEach(item => items.push(item));
|
||||||
|
|
||||||
|
insertLauncher(items);
|
||||||
|
|
||||||
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
|
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
|
||||||
items.push({
|
items.push({
|
||||||
uniqueKey: "separator_grouped",
|
uniqueKey: "separator_grouped",
|
||||||
@@ -148,21 +227,26 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unpinnedGroups.forEach(item => items.push(item));
|
unpinnedGroups.forEach(item => items.push(item));
|
||||||
root.pinnedAppCount = pinnedGroups.length;
|
root.pinnedAppCount = pinnedGroups.length + (SettingsData.dockLauncherEnabled ? 1 : 0);
|
||||||
} else {
|
} else {
|
||||||
pinnedApps.forEach(rawAppId => {
|
pinnedApps.forEach(rawAppId => {
|
||||||
const appId = Paths.moddedAppId(rawAppId);
|
const appId = Paths.moddedAppId(rawAppId);
|
||||||
|
const coreAppData = getCoreAppData(appId);
|
||||||
items.push({
|
items.push({
|
||||||
uniqueKey: "pinned_" + appId,
|
uniqueKey: "pinned_" + appId,
|
||||||
type: "pinned",
|
type: "pinned",
|
||||||
appId: appId,
|
appId: appId,
|
||||||
toplevel: null,
|
toplevel: null,
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
isRunning: false
|
isRunning: false,
|
||||||
|
isCoreApp: coreAppData !== null,
|
||||||
|
coreAppData: coreAppData
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
root.pinnedAppCount = pinnedApps.length;
|
root.pinnedAppCount = pinnedApps.length + (SettingsData.dockLauncherEnabled ? 1 : 0);
|
||||||
|
|
||||||
|
insertLauncher(items);
|
||||||
|
|
||||||
if (pinnedApps.length > 0 && sortedToplevels.length > 0) {
|
if (pinnedApps.length > 0 && sortedToplevels.length > 0) {
|
||||||
items.push({
|
items.push({
|
||||||
@@ -187,13 +271,31 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rawAppId = toplevel.appId || "unknown";
|
||||||
|
const moddedAppId = Paths.moddedAppId(rawAppId);
|
||||||
|
|
||||||
|
// Check if this is a core app window (e.g., Settings modal with appId "org.quickshell")
|
||||||
|
let coreAppData = null;
|
||||||
|
let isCoreApp = false;
|
||||||
|
if (rawAppId === "org.quickshell") {
|
||||||
|
coreAppData = getCoreAppDataByTitle(toplevel.title);
|
||||||
|
if (coreAppData) {
|
||||||
|
isCoreApp = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalAppId = isCoreApp ? coreAppData.builtInPluginId : moddedAppId;
|
||||||
|
const isPinned = pinnedApps.indexOf(finalAppId) !== -1;
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
uniqueKey: uniqueKey,
|
uniqueKey: uniqueKey,
|
||||||
type: "window",
|
type: "window",
|
||||||
appId: Paths.moddedAppId(toplevel.appId),
|
appId: finalAppId,
|
||||||
toplevel: toplevel,
|
toplevel: toplevel,
|
||||||
isPinned: false,
|
isPinned: isPinned,
|
||||||
isRunning: true
|
isRunning: true,
|
||||||
|
isCoreApp: isCoreApp,
|
||||||
|
coreAppData: coreAppData
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -203,10 +305,10 @@ Item {
|
|||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
id: delegateItem
|
id: delegateItem
|
||||||
property alias dockButton: button
|
property var dockButton: itemData.type === "launcher" ? launcherButton : button
|
||||||
property var itemData: modelData
|
property var itemData: modelData
|
||||||
clip: false
|
clip: false
|
||||||
z: button.dragging ? 100 : 0
|
z: (itemData.type === "launcher" ? launcherButton.dragging : button.dragging) ? 100 : 0
|
||||||
|
|
||||||
width: itemData.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2)
|
width: itemData.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2)
|
||||||
height: itemData.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize)
|
height: itemData.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize)
|
||||||
@@ -261,9 +363,22 @@ Item {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DockLauncherButton {
|
||||||
|
id: launcherButton
|
||||||
|
visible: itemData.type === "launcher"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
width: delegateItem.width
|
||||||
|
height: delegateItem.height
|
||||||
|
actualIconSize: root.iconSize
|
||||||
|
|
||||||
|
dockApps: root
|
||||||
|
index: model.index
|
||||||
|
}
|
||||||
|
|
||||||
DockAppButton {
|
DockAppButton {
|
||||||
id: button
|
id: button
|
||||||
visible: itemData.type !== "separator"
|
visible: itemData.type !== "separator" && itemData.type !== "launcher"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
||||||
width: delegateItem.width
|
width: delegateItem.width
|
||||||
@@ -314,5 +429,27 @@ Item {
|
|||||||
function onDockIsolateDisplaysChanged() {
|
function onDockIsolateDisplaysChanged() {
|
||||||
repeater.updateModel();
|
repeater.updateModel();
|
||||||
}
|
}
|
||||||
|
function onDockLauncherEnabledChanged() {
|
||||||
|
root.suppressShiftAnimation = true;
|
||||||
|
root.draggedIndex = -1;
|
||||||
|
root.dropTargetIndex = -1;
|
||||||
|
repeater.updateModel();
|
||||||
|
Qt.callLater(() => {
|
||||||
|
root.suppressShiftAnimation = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SessionData
|
||||||
|
function onDockLauncherPositionChanged() {
|
||||||
|
root.suppressShiftAnimation = true;
|
||||||
|
root.draggedIndex = -1;
|
||||||
|
root.dropTargetIndex = -1;
|
||||||
|
repeater.updateModel();
|
||||||
|
Qt.callLater(() => {
|
||||||
|
root.suppressShiftAnimation = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
294
quickshell/Modules/Dock/DockLauncherButton.qml
Normal file
294
quickshell/Modules/Dock/DockLauncherButton.qml
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
clip: false
|
||||||
|
property var dockApps: null
|
||||||
|
property int index: -1
|
||||||
|
property bool longPressing: false
|
||||||
|
property bool dragging: false
|
||||||
|
property point dragStartPos: Qt.point(0, 0)
|
||||||
|
property real dragAxisOffset: 0
|
||||||
|
property int targetIndex: -1
|
||||||
|
property int originalIndex: -1
|
||||||
|
property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
||||||
|
property bool isHovered: mouseArea.containsMouse && !dragging
|
||||||
|
property bool showTooltip: mouseArea.containsMouse && !dragging
|
||||||
|
property real actualIconSize: 40
|
||||||
|
|
||||||
|
readonly property string tooltipText: I18n.tr("Applications")
|
||||||
|
|
||||||
|
readonly property color effectiveLogoColor: {
|
||||||
|
const override = SettingsData.dockLauncherLogoColorOverride;
|
||||||
|
if (override === "primary")
|
||||||
|
return Theme.primary;
|
||||||
|
if (override === "surface")
|
||||||
|
return Theme.surfaceText;
|
||||||
|
if (override !== "")
|
||||||
|
return override;
|
||||||
|
return Theme.surfaceText;
|
||||||
|
}
|
||||||
|
|
||||||
|
onIsHoveredChanged: {
|
||||||
|
if (mouseArea.pressed || dragging)
|
||||||
|
return;
|
||||||
|
if (isHovered) {
|
||||||
|
exitAnimation.stop();
|
||||||
|
if (!bounceAnimation.running) {
|
||||||
|
bounceAnimation.restart();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bounceAnimation.stop();
|
||||||
|
exitAnimation.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property bool animateX: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
||||||
|
readonly property real animationDistance: actualIconSize
|
||||||
|
readonly property real animationDirection: {
|
||||||
|
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
|
||||||
|
return -1;
|
||||||
|
if (SettingsData.dockPosition === SettingsData.Position.Top)
|
||||||
|
return 1;
|
||||||
|
if (SettingsData.dockPosition === SettingsData.Position.Right)
|
||||||
|
return -1;
|
||||||
|
if (SettingsData.dockPosition === SettingsData.Position.Left)
|
||||||
|
return 1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
id: bounceAnimation
|
||||||
|
|
||||||
|
running: false
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
target: root
|
||||||
|
property: "hoverAnimOffset"
|
||||||
|
to: animationDirection * animationDistance * 0.25
|
||||||
|
duration: Anims.durShort
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Anims.emphasizedAccel
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
target: root
|
||||||
|
property: "hoverAnimOffset"
|
||||||
|
to: animationDirection * animationDistance * 0.2
|
||||||
|
duration: Anims.durShort
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Anims.emphasizedDecel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
id: exitAnimation
|
||||||
|
|
||||||
|
running: false
|
||||||
|
target: root
|
||||||
|
property: "hoverAnimOffset"
|
||||||
|
to: 0
|
||||||
|
duration: Anims.durShort
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Anims.emphasizedDecel
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: longPressTimer
|
||||||
|
|
||||||
|
interval: 500
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
longPressing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: true
|
||||||
|
preventStealing: true
|
||||||
|
cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
onPressed: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
dragStartPos = Qt.point(mouse.x, mouse.y);
|
||||||
|
longPressTimer.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onReleased: mouse => {
|
||||||
|
longPressTimer.stop();
|
||||||
|
|
||||||
|
const wasDragging = dragging;
|
||||||
|
const didReorder = wasDragging && targetIndex >= 0 && dockApps;
|
||||||
|
|
||||||
|
if (didReorder) {
|
||||||
|
SessionData.setDockLauncherPosition(targetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
longPressing = false;
|
||||||
|
dragging = false;
|
||||||
|
dragAxisOffset = 0;
|
||||||
|
targetIndex = -1;
|
||||||
|
originalIndex = -1;
|
||||||
|
|
||||||
|
if (dockApps) {
|
||||||
|
dockApps.draggedIndex = -1;
|
||||||
|
dockApps.dropTargetIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasDragging || mouse.button !== Qt.LeftButton)
|
||||||
|
return;
|
||||||
|
|
||||||
|
PopoutService.toggleDankLauncherV2();
|
||||||
|
}
|
||||||
|
onPositionChanged: mouse => {
|
||||||
|
if (longPressing && !dragging) {
|
||||||
|
const distance = Math.sqrt(Math.pow(mouse.x - dragStartPos.x, 2) + Math.pow(mouse.y - dragStartPos.y, 2));
|
||||||
|
if (distance > 5) {
|
||||||
|
dragging = true;
|
||||||
|
targetIndex = index;
|
||||||
|
originalIndex = index;
|
||||||
|
if (dockApps) {
|
||||||
|
dockApps.draggedIndex = index;
|
||||||
|
dockApps.dropTargetIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dragging || !dockApps)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const axisOffset = isVertical ? (mouse.y - dragStartPos.y) : (mouse.x - dragStartPos.x);
|
||||||
|
dragAxisOffset = axisOffset;
|
||||||
|
|
||||||
|
const spacing = Math.min(8, Math.max(4, actualIconSize * 0.08));
|
||||||
|
const itemSize = actualIconSize * 1.2 + spacing;
|
||||||
|
const slotOffset = Math.round(axisOffset / itemSize);
|
||||||
|
const newTargetIndex = Math.max(0, Math.min(dockApps.pinnedAppCount, originalIndex + slotOffset));
|
||||||
|
|
||||||
|
if (newTargetIndex !== targetIndex) {
|
||||||
|
targetIndex = newTargetIndex;
|
||||||
|
dockApps.dropTargetIndex = newTargetIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property real hoverAnimOffset: 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: visualContent
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
transform: Translate {
|
||||||
|
x: dragging && !isVertical ? dragAxisOffset : (!dragging && isVertical ? hoverAnimOffset : 0)
|
||||||
|
y: dragging && isVertical ? dragAxisOffset : (!dragging && !isVertical ? hoverAnimOffset : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: actualIconSize
|
||||||
|
height: actualIconSize
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
visible: SettingsData.dockLauncherLogoMode === "apps"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "apps"
|
||||||
|
size: actualIconSize - 4
|
||||||
|
color: Theme.widgetIconColor
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemLogo {
|
||||||
|
visible: SettingsData.dockLauncherLogoMode === "os"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
|
||||||
|
height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
|
||||||
|
colorOverride: effectiveLogoColor
|
||||||
|
brightnessOverride: SettingsData.dockLauncherLogoBrightness
|
||||||
|
contrastOverride: SettingsData.dockLauncherLogoContrast
|
||||||
|
}
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
visible: SettingsData.dockLauncherLogoMode === "dank"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
|
||||||
|
height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
|
asynchronous: true
|
||||||
|
source: "file://" + Theme.shellDir + "/assets/danklogo.svg"
|
||||||
|
layer.enabled: effectiveLogoColor !== ""
|
||||||
|
layer.smooth: true
|
||||||
|
layer.mipmap: true
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
saturation: 0
|
||||||
|
colorization: 1
|
||||||
|
colorizationColor: effectiveLogoColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
visible: SettingsData.dockLauncherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isLabwc)
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
|
||||||
|
height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
source: {
|
||||||
|
if (CompositorService.isNiri) {
|
||||||
|
return "file://" + Theme.shellDir + "/assets/niri.svg";
|
||||||
|
} else if (CompositorService.isHyprland) {
|
||||||
|
return "file://" + Theme.shellDir + "/assets/hyprland.svg";
|
||||||
|
} else if (CompositorService.isDwl) {
|
||||||
|
return "file://" + Theme.shellDir + "/assets/mango.png";
|
||||||
|
} else if (CompositorService.isSway) {
|
||||||
|
return "file://" + Theme.shellDir + "/assets/sway.svg";
|
||||||
|
} else if (CompositorService.isScroll) {
|
||||||
|
return "file://" + Theme.shellDir + "/assets/sway.svg";
|
||||||
|
} else if (CompositorService.isLabwc) {
|
||||||
|
return "file://" + Theme.shellDir + "/assets/labwc.png";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
layer.enabled: effectiveLogoColor !== ""
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
saturation: 0
|
||||||
|
colorization: 1
|
||||||
|
colorizationColor: effectiveLogoColor
|
||||||
|
brightness: {
|
||||||
|
SettingsData.dockLauncherLogoBrightness;
|
||||||
|
}
|
||||||
|
contrast: {
|
||||||
|
SettingsData.dockLauncherLogoContrast;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
visible: SettingsData.dockLauncherLogoMode === "custom" && SettingsData.dockLauncherLogoCustomPath !== ""
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
|
||||||
|
height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
source: SettingsData.dockLauncherLogoCustomPath ? "file://" + SettingsData.dockLauncherLogoCustomPath.replace("file://", "") : ""
|
||||||
|
layer.enabled: effectiveLogoColor !== ""
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
saturation: 0
|
||||||
|
colorization: 1
|
||||||
|
colorizationColor: effectiveLogoColor
|
||||||
|
brightness: SettingsData.dockLauncherLogoBrightness
|
||||||
|
contrast: SettingsData.dockLauncherLogoContrast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -164,6 +164,265 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "apps"
|
||||||
|
title: I18n.tr("Launcher Button")
|
||||||
|
settingKey: "dockLauncher"
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "dockLauncherEnabled"
|
||||||
|
tags: ["dock", "launcher", "button", "apps"]
|
||||||
|
text: I18n.tr("Show Launcher Button")
|
||||||
|
description: I18n.tr("Add a draggable launcher button to the dock")
|
||||||
|
checked: SettingsData.dockLauncherEnabled
|
||||||
|
onToggled: checked => SettingsData.set("dockLauncherEnabled", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
visible: SettingsData.dockLauncherEnabled
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("Long press and drag the launcher button to reposition it in the dock")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Launcher Icon")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: logoModeGroup.implicitHeight
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
DankButtonGroup {
|
||||||
|
id: logoModeGroup
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
|
||||||
|
minButtonWidth: parent.width < 480 ? 44 : 64
|
||||||
|
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
||||||
|
model: {
|
||||||
|
const modes = [I18n.tr("Apps Icon"), I18n.tr("OS Logo"), I18n.tr("Dank")];
|
||||||
|
if (CompositorService.isNiri) {
|
||||||
|
modes.push("niri");
|
||||||
|
} else if (CompositorService.isHyprland) {
|
||||||
|
modes.push("Hyprland");
|
||||||
|
} else if (CompositorService.isDwl) {
|
||||||
|
modes.push("mango");
|
||||||
|
} else if (CompositorService.isSway) {
|
||||||
|
modes.push("Sway");
|
||||||
|
} else if (CompositorService.isScroll) {
|
||||||
|
modes.push("Scroll");
|
||||||
|
} else {
|
||||||
|
modes.push(I18n.tr("Compositor"));
|
||||||
|
}
|
||||||
|
modes.push(I18n.tr("Custom"));
|
||||||
|
return modes;
|
||||||
|
}
|
||||||
|
currentIndex: {
|
||||||
|
if (SettingsData.dockLauncherLogoMode === "apps")
|
||||||
|
return 0;
|
||||||
|
if (SettingsData.dockLauncherLogoMode === "os")
|
||||||
|
return 1;
|
||||||
|
if (SettingsData.dockLauncherLogoMode === "dank")
|
||||||
|
return 2;
|
||||||
|
if (SettingsData.dockLauncherLogoMode === "compositor")
|
||||||
|
return 3;
|
||||||
|
if (SettingsData.dockLauncherLogoMode === "custom")
|
||||||
|
return 4;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
onSelectionChanged: (index, selected) => {
|
||||||
|
if (!selected)
|
||||||
|
return;
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
SettingsData.set("dockLauncherLogoMode", "apps");
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
SettingsData.set("dockLauncherLogoMode", "os");
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
SettingsData.set("dockLauncherLogoMode", "dank");
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
SettingsData.set("dockLauncherLogoMode", "compositor");
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
SettingsData.set("dockLauncherLogoMode", "custom");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
visible: SettingsData.dockLauncherLogoMode !== "apps"
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Color Override")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: colorOverrideRow.implicitHeight
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: colorOverrideRow
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankButtonGroup {
|
||||||
|
id: colorModeGroup
|
||||||
|
buttonPadding: parent.parent.width < 480 ? Theme.spacingS : Theme.spacingL
|
||||||
|
minButtonWidth: parent.parent.width < 480 ? 44 : 64
|
||||||
|
textSize: parent.parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
||||||
|
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
|
||||||
|
currentIndex: {
|
||||||
|
const override = SettingsData.dockLauncherLogoColorOverride;
|
||||||
|
if (override === "")
|
||||||
|
return 0;
|
||||||
|
if (override === "primary")
|
||||||
|
return 1;
|
||||||
|
if (override === "surface")
|
||||||
|
return 2;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
onSelectionChanged: (index, selected) => {
|
||||||
|
if (!selected)
|
||||||
|
return;
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
SettingsData.set("dockLauncherLogoColorOverride", "");
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
SettingsData.set("dockLauncherLogoColorOverride", "primary");
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
SettingsData.set("dockLauncherLogoColorOverride", "surface");
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
const currentOverride = SettingsData.dockLauncherLogoColorOverride;
|
||||||
|
const isPreset = currentOverride === "" || currentOverride === "primary" || currentOverride === "surface";
|
||||||
|
if (isPreset) {
|
||||||
|
SettingsData.set("dockLauncherLogoColorOverride", "#ffffff");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: colorPickerCircle
|
||||||
|
visible: {
|
||||||
|
const override = SettingsData.dockLauncherLogoColorOverride;
|
||||||
|
return override !== "" && override !== "primary" && override !== "surface";
|
||||||
|
}
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: 18
|
||||||
|
color: {
|
||||||
|
const override = SettingsData.dockLauncherLogoColorOverride;
|
||||||
|
if (override !== "" && override !== "primary" && override !== "surface")
|
||||||
|
return override;
|
||||||
|
return "#ffffff";
|
||||||
|
}
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (!PopoutService.colorPickerModal)
|
||||||
|
return;
|
||||||
|
PopoutService.colorPickerModal.selectedColor = SettingsData.dockLauncherLogoColorOverride;
|
||||||
|
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Choose Dock Launcher Logo Color");
|
||||||
|
PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) {
|
||||||
|
SettingsData.set("dockLauncherLogoColorOverride", selectedColor);
|
||||||
|
};
|
||||||
|
PopoutService.colorPickerModal.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsSliderRow {
|
||||||
|
settingKey: "dockLauncherLogoSizeOffset"
|
||||||
|
tags: ["dock", "launcher", "logo", "size", "offset", "scale"]
|
||||||
|
text: I18n.tr("Size Offset")
|
||||||
|
minimum: -12
|
||||||
|
maximum: 12
|
||||||
|
value: SettingsData.dockLauncherLogoSizeOffset
|
||||||
|
defaultValue: 0
|
||||||
|
onSliderValueChanged: newValue => SettingsData.set("dockLauncherLogoSizeOffset", newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
visible: {
|
||||||
|
const override = SettingsData.dockLauncherLogoColorOverride;
|
||||||
|
return override !== "" && override !== "primary" && override !== "surface";
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsSliderRow {
|
||||||
|
settingKey: "dockLauncherLogoBrightness"
|
||||||
|
tags: ["dock", "launcher", "logo", "brightness", "color"]
|
||||||
|
text: I18n.tr("Brightness")
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
value: Math.round(SettingsData.dockLauncherLogoBrightness * 100)
|
||||||
|
unit: "%"
|
||||||
|
defaultValue: 50
|
||||||
|
onSliderValueChanged: newValue => SettingsData.set("dockLauncherLogoBrightness", newValue / 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsSliderRow {
|
||||||
|
settingKey: "dockLauncherLogoContrast"
|
||||||
|
tags: ["dock", "launcher", "logo", "contrast", "color"]
|
||||||
|
text: I18n.tr("Contrast")
|
||||||
|
minimum: 0
|
||||||
|
maximum: 200
|
||||||
|
value: Math.round(SettingsData.dockLauncherLogoContrast * 100)
|
||||||
|
unit: "%"
|
||||||
|
defaultValue: 100
|
||||||
|
onSliderValueChanged: newValue => SettingsData.set("dockLauncherLogoContrast", newValue / 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
iconName: "photo_size_select_large"
|
iconName: "photo_size_select_large"
|
||||||
|
|||||||
@@ -259,7 +259,8 @@ Item {
|
|||||||
minButtonWidth: parent.width < 420 ? 44 : 64
|
minButtonWidth: parent.width < 420 ? 44 : 64
|
||||||
textSize: parent.width < 420 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
textSize: parent.width < 420 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
||||||
property bool isRegistryTheme: Theme.currentThemeCategory === "registry"
|
property bool isRegistryTheme: Theme.currentThemeCategory === "registry"
|
||||||
property int currentThemeIndex: {
|
property int pendingIndex: -1
|
||||||
|
property int computedIndex: {
|
||||||
if (isRegistryTheme)
|
if (isRegistryTheme)
|
||||||
return 3;
|
return 3;
|
||||||
if (Theme.currentTheme === Theme.dynamic)
|
if (Theme.currentTheme === Theme.dynamic)
|
||||||
@@ -268,20 +269,21 @@ Item {
|
|||||||
return 2;
|
return 2;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
property int pendingThemeIndex: -1
|
|
||||||
|
|
||||||
model: DMSService.dmsAvailable ? ["Generic", "Auto", "Custom", "Browse"] : ["Generic", "Auto", "Custom"]
|
model: DMSService.dmsAvailable ? ["Generic", "Auto", "Custom", "Browse"] : ["Generic", "Auto", "Custom"]
|
||||||
currentIndex: currentThemeIndex
|
currentIndex: pendingIndex >= 0 ? pendingIndex : computedIndex
|
||||||
selectionMode: "single"
|
selectionMode: "single"
|
||||||
onSelectionChanged: (index, selected) => {
|
onSelectionChanged: (index, selected) => {
|
||||||
if (!selected)
|
if (!selected)
|
||||||
return;
|
return;
|
||||||
pendingThemeIndex = index;
|
pendingIndex = index;
|
||||||
}
|
}
|
||||||
onAnimationCompleted: {
|
onAnimationCompleted: {
|
||||||
if (pendingThemeIndex === -1)
|
if (pendingIndex < 0)
|
||||||
return;
|
return;
|
||||||
switch (pendingThemeIndex) {
|
const idx = pendingIndex;
|
||||||
|
pendingIndex = -1;
|
||||||
|
switch (idx) {
|
||||||
case 0:
|
case 0:
|
||||||
Theme.switchThemeCategory("generic", "blue");
|
Theme.switchThemeCategory("generic", "blue");
|
||||||
break;
|
break;
|
||||||
@@ -300,7 +302,6 @@ Item {
|
|||||||
Theme.switchThemeCategory("registry", "");
|
Theme.switchThemeCategory("registry", "");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
pendingThemeIndex = -1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -809,13 +810,21 @@ Item {
|
|||||||
buttonPadding: parent.width < 400 ? Theme.spacingS : Theme.spacingL
|
buttonPadding: parent.width < 400 ? Theme.spacingS : Theme.spacingL
|
||||||
minButtonWidth: parent.width < 400 ? 44 : 64
|
minButtonWidth: parent.width < 400 ? 44 : 64
|
||||||
textSize: parent.width < 400 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
textSize: parent.width < 400 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
||||||
|
property int pendingIndex: -1
|
||||||
model: variantSelector.flavorNames
|
model: variantSelector.flavorNames
|
||||||
currentIndex: variantSelector.flavorIndex
|
currentIndex: pendingIndex >= 0 ? pendingIndex : variantSelector.flavorIndex
|
||||||
selectionMode: "single"
|
selectionMode: "single"
|
||||||
onAnimationCompleted: {
|
onSelectionChanged: (index, selected) => {
|
||||||
if (currentIndex < 0 || currentIndex >= variantSelector.flavorOptions.length)
|
if (!selected)
|
||||||
return;
|
return;
|
||||||
const flavorId = variantSelector.flavorOptions[currentIndex]?.id;
|
pendingIndex = index;
|
||||||
|
}
|
||||||
|
onAnimationCompleted: {
|
||||||
|
if (pendingIndex < 0 || pendingIndex >= variantSelector.flavorOptions.length)
|
||||||
|
return;
|
||||||
|
const flavorId = variantSelector.flavorOptions[pendingIndex]?.id;
|
||||||
|
const idx = pendingIndex;
|
||||||
|
pendingIndex = -1;
|
||||||
if (!flavorId || flavorId === variantSelector.selectedFlavor)
|
if (!flavorId || flavorId === variantSelector.selectedFlavor)
|
||||||
return;
|
return;
|
||||||
Theme.screenTransition();
|
Theme.screenTransition();
|
||||||
@@ -909,13 +918,21 @@ Item {
|
|||||||
buttonPadding: parent.width < 400 ? Theme.spacingS : Theme.spacingL
|
buttonPadding: parent.width < 400 ? Theme.spacingS : Theme.spacingL
|
||||||
minButtonWidth: parent.width < 400 ? 44 : 64
|
minButtonWidth: parent.width < 400 ? 44 : 64
|
||||||
textSize: parent.width < 400 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
textSize: parent.width < 400 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
||||||
|
property int pendingIndex: -1
|
||||||
model: variantSelector.variantNames
|
model: variantSelector.variantNames
|
||||||
currentIndex: variantSelector.selectedIndex
|
currentIndex: pendingIndex >= 0 ? pendingIndex : variantSelector.selectedIndex
|
||||||
selectionMode: "single"
|
selectionMode: "single"
|
||||||
onAnimationCompleted: {
|
onSelectionChanged: (index, selected) => {
|
||||||
if (currentIndex < 0 || !variantSelector.activeThemeVariants?.options)
|
if (!selected)
|
||||||
return;
|
return;
|
||||||
const variantId = variantSelector.activeThemeVariants.options[currentIndex]?.id;
|
pendingIndex = index;
|
||||||
|
}
|
||||||
|
onAnimationCompleted: {
|
||||||
|
if (pendingIndex < 0 || !variantSelector.activeThemeVariants?.options)
|
||||||
|
return;
|
||||||
|
const variantId = variantSelector.activeThemeVariants.options[pendingIndex]?.id;
|
||||||
|
const idx = pendingIndex;
|
||||||
|
pendingIndex = -1;
|
||||||
if (!variantId || variantId === variantSelector.selectedVariant)
|
if (!variantId || variantId === variantSelector.selectedVariant)
|
||||||
return;
|
return;
|
||||||
Theme.screenTransition();
|
Theme.screenTransition();
|
||||||
|
|||||||
@@ -363,6 +363,25 @@ Item {
|
|||||||
onToggled: checked => SettingsData.set("useFahrenheit", checked)
|
onToggled: checked => SettingsData.set("useFahrenheit", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Theme.outline
|
||||||
|
opacity: 0.15
|
||||||
|
visible: !SettingsData.useFahrenheit
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tab: "time"
|
||||||
|
tags: ["weather", "wind", "speed", "units", "metric"]
|
||||||
|
settingKey: "windSpeedUnit"
|
||||||
|
text: I18n.tr("Wind Speed in m/s")
|
||||||
|
description: I18n.tr("Use meters per second instead of km/h for wind speed")
|
||||||
|
checked: SettingsData.windSpeedUnit === "ms"
|
||||||
|
onToggled: checked => SettingsData.set("windSpeedUnit", checked ? "ms" : "kmh")
|
||||||
|
visible: !SettingsData.useFahrenheit
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 1
|
height: 1
|
||||||
@@ -689,6 +708,13 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
property var feelsLike: SettingsData.useFahrenheit ? (WeatherService.weather.feelsLikeF || WeatherService.weather.tempF) : (WeatherService.weather.feelsLike || WeatherService.weather.temp)
|
||||||
|
text: I18n.tr("Feels Like %1°").arg(feelsLike)
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: WeatherService.weather.city || ""
|
text: WeatherService.weather.city || ""
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
@@ -891,20 +917,24 @@ Item {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
}
|
}
|
||||||
StyledText {
|
StyledText {
|
||||||
|
id: windText
|
||||||
text: {
|
text: {
|
||||||
if (!WeatherService.weather.wind)
|
SettingsData.windSpeedUnit;
|
||||||
return "--";
|
SettingsData.useFahrenheit;
|
||||||
const windKmh = parseFloat(WeatherService.weather.wind);
|
return WeatherService.formatSpeed(WeatherService.weather.wind) || "--";
|
||||||
if (isNaN(windKmh))
|
|
||||||
return WeatherService.weather.wind;
|
|
||||||
if (SettingsData.useFahrenheit)
|
|
||||||
return Math.round(windKmh * 0.621371) + " mph";
|
|
||||||
return WeatherService.weather.wind;
|
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall + 1
|
font.pixelSize: Theme.fontSizeSmall + 1
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: SettingsData.useFahrenheit ? Qt.ArrowCursor : Qt.PointingHandCursor
|
||||||
|
enabled: !SettingsData.useFahrenheit
|
||||||
|
onClicked: SettingsData.set("windSpeedUnit", SettingsData.windSpeedUnit === "kmh" ? "ms" : "kmh")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,10 +318,10 @@ Singleton {
|
|||||||
PopoutService.focusOrToggleSettings();
|
PopoutService.focusOrToggleSettings();
|
||||||
return true;
|
return true;
|
||||||
case "notepad":
|
case "notepad":
|
||||||
PopoutService.openNotepad();
|
PopoutService.toggleNotepad();
|
||||||
return true;
|
return true;
|
||||||
case "processlist":
|
case "processlist":
|
||||||
PopoutService.showProcessListModal();
|
PopoutService.toggleProcessList();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -354,9 +354,15 @@ Singleton {
|
|||||||
if (kmh == null) {
|
if (kmh == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const value = SettingsData.useFahrenheit ? Math.round(kmh * 0.621371) : kmh;
|
if (SettingsData.useFahrenheit) {
|
||||||
const unit = SettingsData.useFahrenheit ? "mph" : "km/h";
|
const value = Math.round(kmh * 0.621371);
|
||||||
return includeUnits ? value + " " + unit : value;
|
return includeUnits ? value + " mph" : value;
|
||||||
|
}
|
||||||
|
if (SettingsData.windSpeedUnit === "ms") {
|
||||||
|
const value = (kmh / 3.6).toFixed(1);
|
||||||
|
return includeUnits ? value + " m/s" : value;
|
||||||
|
}
|
||||||
|
return includeUnits ? kmh + " km/h" : kmh;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPressure(hpa, includeUnits = true) {
|
function formatPressure(hpa, includeUnits = true) {
|
||||||
@@ -805,7 +811,7 @@ Singleton {
|
|||||||
"country": root.location?.country || "Unknown",
|
"country": root.location?.country || "Unknown",
|
||||||
"wCode": current.weather_code || 0,
|
"wCode": current.weather_code || 0,
|
||||||
"humidity": Math.round(current.relative_humidity_2m || 0),
|
"humidity": Math.round(current.relative_humidity_2m || 0),
|
||||||
"wind": Math.round(current.wind_speed_10m || 0) + " " + (currentUnits.wind_speed_10m || 'm/s'),
|
"wind": Math.round(current.wind_speed_10m || 0),
|
||||||
"sunrise": formatTime(daily.sunrise?.[0]) || "06:00",
|
"sunrise": formatTime(daily.sunrise?.[0]) || "06:00",
|
||||||
"sunset": formatTime(daily.sunset?.[0]) || "18:00",
|
"sunset": formatTime(daily.sunset?.[0]) || "18:00",
|
||||||
"rawSunrise": daily.sunrise?.[0] || "",
|
"rawSunrise": daily.sunrise?.[0] || "",
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ Item {
|
|||||||
required property int iconSize
|
required property int iconSize
|
||||||
property string fallbackText: "A"
|
property string fallbackText: "A"
|
||||||
property color iconColor: Theme.surfaceText
|
property color iconColor: Theme.surfaceText
|
||||||
|
property color colorOverride: "transparent"
|
||||||
|
property real brightnessOverride: 0.0
|
||||||
|
property real contrastOverride: 0.0
|
||||||
|
property real saturationOverride: 0.0
|
||||||
property color fallbackBackgroundColor: Theme.surfaceLight
|
property color fallbackBackgroundColor: Theme.surfaceLight
|
||||||
property color fallbackTextColor: Theme.primary
|
property color fallbackTextColor: Theme.primary
|
||||||
property real materialIconSizeAdjustment: Theme.spacingM
|
property real materialIconSizeAdjustment: Theme.spacingM
|
||||||
@@ -27,6 +31,7 @@ Item {
|
|||||||
readonly property bool isSvgCorner: iconValue.startsWith("svg+corner:")
|
readonly property bool isSvgCorner: iconValue.startsWith("svg+corner:")
|
||||||
readonly property bool isSvg: !isSvgCorner && iconValue.startsWith("svg:")
|
readonly property bool isSvg: !isSvgCorner && iconValue.startsWith("svg:")
|
||||||
readonly property bool isImage: iconValue.startsWith("image:")
|
readonly property bool isImage: iconValue.startsWith("image:")
|
||||||
|
readonly property bool hasColorOverride: colorOverride.a > 0
|
||||||
readonly property string materialName: isMaterial ? iconValue.substring(9) : ""
|
readonly property string materialName: isMaterial ? iconValue.substring(9) : ""
|
||||||
readonly property string unicodeChar: isUnicode ? iconValue.substring(8) : ""
|
readonly property string unicodeChar: isUnicode ? iconValue.substring(8) : ""
|
||||||
readonly property string imagePath: isImage ? iconValue.substring(6) : ""
|
readonly property string imagePath: isImage ? iconValue.substring(6) : ""
|
||||||
@@ -48,7 +53,7 @@ Item {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: root.materialName
|
name: root.materialName
|
||||||
size: root.iconSize - root.materialIconSizeAdjustment
|
size: root.iconSize - root.materialIconSizeAdjustment
|
||||||
color: root.iconColor
|
color: root.hasColorOverride ? root.colorOverride : root.iconColor
|
||||||
visible: root.isMaterial
|
visible: root.isMaterial
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +61,7 @@ Item {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: root.unicodeChar
|
text: root.unicodeChar
|
||||||
font.pixelSize: root.iconSize * root.unicodeIconScale
|
font.pixelSize: root.iconSize * root.unicodeIconScale
|
||||||
color: root.iconColor
|
color: root.hasColorOverride ? root.colorOverride : root.iconColor
|
||||||
visible: root.isUnicode
|
visible: root.isUnicode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +70,10 @@ Item {
|
|||||||
source: root.svgSource
|
source: root.svgSource
|
||||||
size: root.iconSize
|
size: root.iconSize
|
||||||
cornerIcon: root.svgCornerIcon
|
cornerIcon: root.svgCornerIcon
|
||||||
|
colorOverride: root.colorOverride
|
||||||
|
brightnessOverride: root.brightnessOverride
|
||||||
|
contrastOverride: root.contrastOverride
|
||||||
|
saturationOverride: root.saturationOverride
|
||||||
visible: root.isSvg || root.isSvgCorner
|
visible: root.isSvg || root.isSvgCorner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user