mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-02 02:22:06 -04:00
Compare commits
15 Commits
bcf41ed5ca
...
eb5afcdc40
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb5afcdc40 | ||
|
|
dd2a3f3efc | ||
|
|
161fa2dba5 | ||
|
|
f5f50f7add | ||
|
|
372cf2f566 | ||
|
|
b70acbc283 | ||
|
|
571c1158bf | ||
|
|
ac03a2e4b2 | ||
|
|
07460dc3b7 | ||
|
|
7412fee590 | ||
|
|
85c2954958 | ||
|
|
7c9e805cbe | ||
|
|
6926470b04 | ||
|
|
713ba1efbb | ||
|
|
1919ca7243 |
@@ -4,6 +4,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -130,12 +132,8 @@ func updateArchLinux() error {
|
|||||||
return errdefs.ErrUpdateCancelled
|
return errdefs.ErrUpdateCancelled
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\nRunning: sudo pacman -S %s\n", packageName)
|
fmt.Printf("\nRunning: pacman -S %s\n", packageName)
|
||||||
cmd := exec.Command("sudo", "pacman", "-S", "--noconfirm", packageName)
|
if err := privesc.Run(context.Background(), "", "pacman", "-S", "--noconfirm", packageName); err != nil {
|
||||||
cmd.Stdin = os.Stdin
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
fmt.Printf("Error: Failed to update using pacman: %v\n", err)
|
fmt.Printf("Error: Failed to update using pacman: %v\n", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -479,11 +477,7 @@ func updateDMSBinary() error {
|
|||||||
|
|
||||||
fmt.Printf("Installing to %s...\n", currentPath)
|
fmt.Printf("Installing to %s...\n", currentPath)
|
||||||
|
|
||||||
replaceCmd := exec.Command("sudo", "install", "-m", "0755", decompressedPath, currentPath)
|
if err := privesc.Run(context.Background(), "", "install", "-m", "0755", decompressedPath, currentPath); err != nil {
|
||||||
replaceCmd.Stdin = os.Stdin
|
|
||||||
replaceCmd.Stdout = os.Stdout
|
|
||||||
replaceCmd.Stderr = os.Stderr
|
|
||||||
if err := replaceCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to replace binary: %w", err)
|
return fmt.Errorf("failed to replace binary: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
@@ -29,7 +31,7 @@ var greeterInstallCmd = &cobra.Command{
|
|||||||
Use: "install",
|
Use: "install",
|
||||||
Short: "Install and configure DMS greeter",
|
Short: "Install and configure DMS greeter",
|
||||||
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
||||||
PreRunE: requireMutableSystemCommand,
|
PreRunE: preRunPrivileged,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
@@ -51,9 +53,10 @@ var greeterInstallCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var greeterSyncCmd = &cobra.Command{
|
var greeterSyncCmd = &cobra.Command{
|
||||||
Use: "sync",
|
Use: "sync",
|
||||||
Short: "Sync DMS theme and settings with greeter",
|
Short: "Sync DMS theme and settings with greeter",
|
||||||
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
|
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
|
||||||
|
PreRunE: preRunPrivileged,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
auth, _ := cmd.Flags().GetBool("auth")
|
auth, _ := cmd.Flags().GetBool("auth")
|
||||||
@@ -82,7 +85,7 @@ var greeterEnableCmd = &cobra.Command{
|
|||||||
Use: "enable",
|
Use: "enable",
|
||||||
Short: "Enable DMS greeter in greetd config",
|
Short: "Enable DMS greeter in greetd config",
|
||||||
Long: "Configure greetd to use DMS as the greeter",
|
Long: "Configure greetd to use DMS as the greeter",
|
||||||
PreRunE: requireMutableSystemCommand,
|
PreRunE: preRunPrivileged,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
@@ -118,7 +121,7 @@ var greeterUninstallCmd = &cobra.Command{
|
|||||||
Use: "uninstall",
|
Use: "uninstall",
|
||||||
Short: "Remove DMS greeter configuration and restore previous display manager",
|
Short: "Remove DMS greeter configuration and restore previous display manager",
|
||||||
Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state",
|
Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state",
|
||||||
PreRunE: requireMutableSystemCommand,
|
PreRunE: preRunPrivileged,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
@@ -288,10 +291,7 @@ func uninstallGreeter(nonInteractive bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nDisabling greetd...")
|
fmt.Println("\nDisabling greetd...")
|
||||||
disableCmd := exec.Command("sudo", "systemctl", "disable", "greetd")
|
if err := privesc.Run(context.Background(), "", "systemctl", "disable", "greetd"); err != nil {
|
||||||
disableCmd.Stdout = os.Stdout
|
|
||||||
disableCmd.Stderr = os.Stderr
|
|
||||||
if err := disableCmd.Run(); err != nil {
|
|
||||||
fmt.Printf(" ⚠ Could not disable greetd: %v\n", err)
|
fmt.Printf(" ⚠ Could not disable greetd: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(" ✓ greetd disabled")
|
fmt.Println(" ✓ greetd disabled")
|
||||||
@@ -357,10 +357,10 @@ func restorePreDMSGreetdConfig(sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
tmp.Close()
|
tmp.Close()
|
||||||
|
|
||||||
if err := runSudoCommand(sudoPassword, "cp", tmpPath, configPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, configPath); err != nil {
|
||||||
return fmt.Errorf("failed to restore %s: %w", candidate, err)
|
return fmt.Errorf("failed to restore %s: %w", candidate, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCommand(sudoPassword, "chmod", "644", configPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", configPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf(" ✓ Restored greetd config from %s\n", candidate)
|
fmt.Printf(" ✓ Restored greetd config from %s\n", candidate)
|
||||||
@@ -388,21 +388,14 @@ command = "agreety --cmd /bin/bash"
|
|||||||
}
|
}
|
||||||
tmp.Close()
|
tmp.Close()
|
||||||
|
|
||||||
if err := runSudoCommand(sudoPassword, "cp", tmpPath, configPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, configPath); err != nil {
|
||||||
return fmt.Errorf("failed to write fallback greetd config: %w", err)
|
return fmt.Errorf("failed to write fallback greetd config: %w", err)
|
||||||
}
|
}
|
||||||
_ = runSudoCommand(sudoPassword, "chmod", "644", configPath)
|
_ = privesc.Run(context.Background(), sudoPassword, "chmod", "644", configPath)
|
||||||
fmt.Println(" ✓ Wrote minimal fallback greetd config (configure a greeter command manually if needed)")
|
fmt.Println(" ✓ Wrote minimal fallback greetd config (configure a greeter command manually if needed)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSudoCommand(_ string, command string, args ...string) error {
|
|
||||||
cmd := exec.Command("sudo", append([]string{command}, args...)...)
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// suggestDisplayManagerRestore scans for installed DMs and re-enables one
|
// suggestDisplayManagerRestore scans for installed DMs and re-enables one
|
||||||
func suggestDisplayManagerRestore(nonInteractive bool) {
|
func suggestDisplayManagerRestore(nonInteractive bool) {
|
||||||
knownDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
knownDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
||||||
@@ -421,10 +414,7 @@ func suggestDisplayManagerRestore(nonInteractive bool) {
|
|||||||
|
|
||||||
enableDM := func(dm string) {
|
enableDM := func(dm string) {
|
||||||
fmt.Printf(" Enabling %s...\n", dm)
|
fmt.Printf(" Enabling %s...\n", dm)
|
||||||
cmd := exec.Command("sudo", "systemctl", "enable", "--force", dm)
|
if err := privesc.Run(context.Background(), "", "systemctl", "enable", "--force", dm); err != nil {
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
fmt.Printf(" ⚠ Failed to enable %s: %v\n", dm, err)
|
fmt.Printf(" ⚠ Failed to enable %s: %v\n", dm, err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" ✓ %s enabled (will take effect on next boot).\n", dm)
|
fmt.Printf(" ✓ %s enabled (will take effect on next boot).\n", dm)
|
||||||
@@ -623,10 +613,7 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
|||||||
|
|
||||||
if response != "n" && response != "no" {
|
if response != "n" && response != "no" {
|
||||||
fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
|
fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
|
||||||
addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username)
|
if err := privesc.Run(context.Background(), "", "usermod", "-aG", greeterGroup, currentUser.Username); err != nil {
|
||||||
addUserCmd.Stdout = os.Stdout
|
|
||||||
addUserCmd.Stderr = os.Stderr
|
|
||||||
if err := addUserCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
|
return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
|
||||||
}
|
}
|
||||||
fmt.Printf("✓ User added to %s group\n", greeterGroup)
|
fmt.Printf("✓ User added to %s group\n", greeterGroup)
|
||||||
@@ -846,22 +833,19 @@ func disableDisplayManager(dmName string) (bool, error) {
|
|||||||
actionTaken := false
|
actionTaken := false
|
||||||
|
|
||||||
if state.NeedsDisable {
|
if state.NeedsDisable {
|
||||||
var disableCmd *exec.Cmd
|
var action, actionVerb string
|
||||||
var actionVerb string
|
switch state.EnabledState {
|
||||||
|
case "static":
|
||||||
if state.EnabledState == "static" {
|
|
||||||
fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName)
|
fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName)
|
||||||
disableCmd = exec.Command("sudo", "systemctl", "mask", dmName)
|
action = "mask"
|
||||||
actionVerb = "masked"
|
actionVerb = "masked"
|
||||||
} else {
|
default:
|
||||||
fmt.Printf(" Disabling %s...\n", dmName)
|
fmt.Printf(" Disabling %s...\n", dmName)
|
||||||
disableCmd = exec.Command("sudo", "systemctl", "disable", dmName)
|
action = "disable"
|
||||||
actionVerb = "disabled"
|
actionVerb = "disabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
disableCmd.Stdout = os.Stdout
|
if err := privesc.Run(context.Background(), "", "systemctl", action, dmName); err != nil {
|
||||||
disableCmd.Stderr = os.Stderr
|
|
||||||
if err := disableCmd.Run(); err != nil {
|
|
||||||
return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err)
|
return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -902,10 +886,7 @@ func ensureGreetdEnabled() error {
|
|||||||
|
|
||||||
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
|
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
|
||||||
fmt.Println(" Unmasking greetd...")
|
fmt.Println(" Unmasking greetd...")
|
||||||
unmaskCmd := exec.Command("sudo", "systemctl", "unmask", "greetd")
|
if err := privesc.Run(context.Background(), "", "systemctl", "unmask", "greetd"); err != nil {
|
||||||
unmaskCmd.Stdout = os.Stdout
|
|
||||||
unmaskCmd.Stderr = os.Stderr
|
|
||||||
if err := unmaskCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to unmask greetd: %w", err)
|
return fmt.Errorf("failed to unmask greetd: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Println(" ✓ Unmasked greetd")
|
fmt.Println(" ✓ Unmasked greetd")
|
||||||
@@ -917,10 +898,7 @@ func ensureGreetdEnabled() error {
|
|||||||
fmt.Println(" Enabling greetd service...")
|
fmt.Println(" Enabling greetd service...")
|
||||||
}
|
}
|
||||||
|
|
||||||
enableCmd := exec.Command("sudo", "systemctl", "enable", "--force", "greetd")
|
if err := privesc.Run(context.Background(), "", "systemctl", "enable", "--force", "greetd"); err != nil {
|
||||||
enableCmd.Stdout = os.Stdout
|
|
||||||
enableCmd.Stderr = os.Stderr
|
|
||||||
if err := enableCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -950,10 +928,7 @@ func ensureGraphicalTarget() error {
|
|||||||
currentTargetStr := strings.TrimSpace(string(currentTarget))
|
currentTargetStr := strings.TrimSpace(string(currentTarget))
|
||||||
if currentTargetStr != "graphical.target" {
|
if currentTargetStr != "graphical.target" {
|
||||||
fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr)
|
fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr)
|
||||||
setDefaultCmd := exec.Command("sudo", "systemctl", "set-default", "graphical.target")
|
if err := privesc.Run(context.Background(), "", "systemctl", "set-default", "graphical.target"); err != nil {
|
||||||
setDefaultCmd.Stdout = os.Stdout
|
|
||||||
setDefaultCmd.Stderr = os.Stderr
|
|
||||||
if err := setDefaultCmd.Run(); err != nil {
|
|
||||||
fmt.Println("⚠ Warning: Failed to set graphical.target as default")
|
fmt.Println("⚠ Warning: Failed to set graphical.target as default")
|
||||||
fmt.Println(" Greeter may not start on boot. Run manually:")
|
fmt.Println(" Greeter may not start on boot. Run manually:")
|
||||||
fmt.Println(" sudo systemctl set-default graphical.target")
|
fmt.Println(" sudo systemctl set-default graphical.target")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -19,7 +21,7 @@ var setupCmd = &cobra.Command{
|
|||||||
Use: "setup",
|
Use: "setup",
|
||||||
Short: "Deploy DMS configurations",
|
Short: "Deploy DMS configurations",
|
||||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||||
PersistentPreRunE: requireMutableSystemCommand,
|
PersistentPreRunE: preRunPrivileged,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := runSetup(); err != nil {
|
if err := runSetup(); err != nil {
|
||||||
log.Fatalf("Error during setup: %v", err)
|
log.Fatalf("Error during setup: %v", err)
|
||||||
@@ -267,6 +269,8 @@ func runSetupDmsConfig(name string) error {
|
|||||||
func runSetup() error {
|
func runSetup() error {
|
||||||
fmt.Println("=== DMS Configuration Setup ===")
|
fmt.Println("=== DMS Configuration Setup ===")
|
||||||
|
|
||||||
|
ensureInputGroup()
|
||||||
|
|
||||||
wm, wmSelected := promptCompositor()
|
wm, wmSelected := promptCompositor()
|
||||||
terminal, terminalSelected := promptTerminal()
|
terminal, terminalSelected := promptTerminal()
|
||||||
useSystemd := promptSystemd()
|
useSystemd := promptSystemd()
|
||||||
@@ -340,6 +344,37 @@ func runSetup() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add user to the input group for the evdev manager for inut state tracking.
|
||||||
|
// Caps Lock OSD and the Caps Lock bar indicator.
|
||||||
|
func ensureInputGroup() {
|
||||||
|
if !utils.HasGroup("input") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentUser := os.Getenv("USER")
|
||||||
|
if currentUser == "" {
|
||||||
|
currentUser = os.Getenv("LOGNAME")
|
||||||
|
}
|
||||||
|
if currentUser == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, err := execGroups(currentUser)
|
||||||
|
if err == nil && strings.Contains(out, "input") {
|
||||||
|
fmt.Printf("✓ %s is already in the input group (Caps Lock OSD enabled)\n", currentUser)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Adding user to input group for Caps Lock OSD support...")
|
||||||
|
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "input", currentUser); err != nil {
|
||||||
|
fmt.Printf("⚠ Could not add %s to input group (Caps Lock OSD will be unavailable): %v\n", currentUser, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✓ Added %s to input group (logout/login required to take effect)\n", currentUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func execGroups(user string) (string, error) {
|
||||||
|
out, err := exec.Command("groups", user).Output()
|
||||||
|
return string(out), err
|
||||||
|
}
|
||||||
|
|
||||||
func promptCompositor() (deps.WindowManager, bool) {
|
func promptCompositor() (deps.WindowManager, bool) {
|
||||||
fmt.Println("Select compositor:")
|
fmt.Println("Select compositor:")
|
||||||
fmt.Println("1) Niri")
|
fmt.Println("1) Niri")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -269,3 +270,16 @@ func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
|
|||||||
|
|
||||||
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// preRunPrivileged combines the immutable-system check with a privesc tool
|
||||||
|
// selection prompt (shown only when multiple tools are available and the
|
||||||
|
// $DMS_PRIVESC env var isn't set).
|
||||||
|
func preRunPrivileged(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := requireMutableSystemCommand(cmd, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := privesc.PromptCLI(os.Stdout, os.Stdin); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -292,7 +293,7 @@ func (a *ArchDistribution) InstallPrerequisites(ctx context.Context, sudoPasswor
|
|||||||
LogOutput: "Installing base-devel development tools",
|
LogOutput: "Installing base-devel development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
|
cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
|
||||||
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
|
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
|
||||||
return fmt.Errorf("failed to install base-devel: %w", err)
|
return fmt.Errorf("failed to install base-devel: %w", err)
|
||||||
}
|
}
|
||||||
@@ -463,7 +464,7 @@ func (a *ArchDistribution) preinstallQuickshellGit(ctx context.Context, sudoPass
|
|||||||
CommandInfo: "sudo pacman -Rdd --noconfirm quickshell",
|
CommandInfo: "sudo pacman -Rdd --noconfirm quickshell",
|
||||||
LogOutput: "Removing stable quickshell so quickshell-git can be installed",
|
LogOutput: "Removing stable quickshell so quickshell-git can be installed",
|
||||||
}
|
}
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell")
|
cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell")
|
||||||
if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil {
|
if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil {
|
||||||
return fmt.Errorf("failed to remove stable quickshell: %w", err)
|
return fmt.Errorf("failed to remove stable quickshell: %w", err)
|
||||||
}
|
}
|
||||||
@@ -501,7 +502,7 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -779,7 +780,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
installArgs := []string{"pacman", "-U", "--noconfirm"}
|
installArgs := []string{"pacman", "-U", "--noconfirm"}
|
||||||
installArgs = append(installArgs, files...)
|
installArgs = append(installArgs, files...)
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
|
||||||
|
|
||||||
fileNames := make([]string, len(files))
|
fileNames := make([]string, len(files))
|
||||||
for i, f := range files {
|
for i, f := range files {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,27 +56,6 @@ func (b *BaseDistribution) logError(message string, err error) {
|
|||||||
b.log(errorMsg)
|
b.log(errorMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// escapeSingleQuotes escapes single quotes in a string for safe use in bash single-quoted strings.
|
|
||||||
// It replaces each ' with '\” which closes the quote, adds an escaped quote, and reopens the quote.
|
|
||||||
// This prevents shell injection and syntax errors when passwords contain single quotes or apostrophes.
|
|
||||||
func escapeSingleQuotes(s string) string {
|
|
||||||
return strings.ReplaceAll(s, "'", "'\\''")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeSudoCommand creates a command string that safely passes password to sudo.
|
|
||||||
// This helper escapes special characters in the password to prevent shell injection
|
|
||||||
// and syntax errors when passwords contain single quotes, apostrophes, or other special chars.
|
|
||||||
func MakeSudoCommand(sudoPassword string, command string) string {
|
|
||||||
return fmt.Sprintf("echo '%s' | sudo -S %s", escapeSingleQuotes(sudoPassword), command)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecSudoCommand creates an exec.Cmd that runs a command with sudo using the provided password.
|
|
||||||
// The password is properly escaped to prevent shell injection and syntax errors.
|
|
||||||
func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd {
|
|
||||||
cmdStr := MakeSudoCommand(sudoPassword, command)
|
|
||||||
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
|
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
|
||||||
status := deps.StatusMissing
|
status := deps.StatusMissing
|
||||||
if b.commandExists(name) {
|
if b.commandExists(name) {
|
||||||
@@ -710,7 +690,7 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Install to /usr/local/bin
|
// Install to /usr/local/bin
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
installCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
|
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install DMS binary: %w", err)
|
return fmt.Errorf("failed to install DMS binary: %w", err)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -191,7 +192,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Updating APT package lists",
|
LogOutput: "Updating APT package lists",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists: %w", err)
|
return fmt.Errorf("failed to update package lists: %w", err)
|
||||||
}
|
}
|
||||||
@@ -208,7 +209,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||||
if err := checkCmd.Run(); err != nil {
|
if err := checkCmd.Run(); err != nil {
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
|
cmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
|
||||||
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||||
}
|
}
|
||||||
@@ -224,7 +225,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Installing additional development tools",
|
LogOutput: "Installing additional development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
@@ -450,7 +451,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
|
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
|
||||||
|
|
||||||
// Create keyrings directory if it doesn't exist
|
// Create keyrings directory if it doesn't exist
|
||||||
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
||||||
if err := mkdirCmd.Run(); err != nil {
|
if err := mkdirCmd.Run(); err != nil {
|
||||||
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
|
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
|
||||||
}
|
}
|
||||||
@@ -464,7 +465,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
|
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
|
cmd := privesc.ExecCommand(ctx, sudoPassword, keyCmd)
|
||||||
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
||||||
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
||||||
}
|
}
|
||||||
@@ -480,7 +481,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
|
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
|
||||||
}
|
}
|
||||||
|
|
||||||
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
|
addRepoCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
|
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
|
||||||
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
|
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
|
||||||
@@ -500,7 +501,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
CommandInfo: "sudo apt-get update",
|
CommandInfo: "sudo apt-get update",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
|
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
|
||||||
}
|
}
|
||||||
@@ -546,7 +547,7 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,7 +635,7 @@ func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
args := []string{"apt-get", "install", "-y"}
|
args := []string{"apt-get", "install", "-y"}
|
||||||
args = append(args, depList...)
|
args = append(args, depList...)
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,7 +653,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
|
|||||||
CommandInfo: "sudo apt-get install rustup",
|
CommandInfo: "sudo apt-get install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
|
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
|
||||||
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
@@ -691,7 +692,7 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get install golang-go",
|
CommandInfo: "sudo apt-get install golang-go",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
|
||||||
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -254,7 +255,7 @@ func (f *FedoraDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
args := []string{"dnf", "install", "-y"}
|
args := []string{"dnf", "install", "-y"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.logError("failed to install prerequisites", err)
|
f.logError("failed to install prerequisites", err)
|
||||||
@@ -437,7 +438,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
|||||||
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
|
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword,
|
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
|
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -461,7 +462,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
|||||||
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
|
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
|
||||||
}
|
}
|
||||||
|
|
||||||
priorityCmd := ExecSudoCommand(ctx, sudoPassword,
|
priorityCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
|
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
|
||||||
priorityOutput, err := priorityCmd.CombinedOutput()
|
priorityOutput, err := priorityCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -537,7 +538,7 @@ func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []st
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var GentooGlobalUseFlags = []string{
|
var GentooGlobalUseFlags = []string{
|
||||||
@@ -201,9 +202,9 @@ func (g *GentooDistribution) setGlobalUseFlags(ctx context.Context, sudoPassword
|
|||||||
|
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
if hasUse {
|
if hasUse {
|
||||||
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
|
cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
|
||||||
} else {
|
} else {
|
||||||
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
|
cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
@@ -281,7 +282,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Syncing Portage tree with emerge --sync",
|
LogOutput: "Syncing Portage tree with emerge --sync",
|
||||||
}
|
}
|
||||||
|
|
||||||
syncCmd := ExecSudoCommand(ctx, sudoPassword, "emerge --sync --quiet")
|
syncCmd := privesc.ExecCommand(ctx, sudoPassword, "emerge --sync --quiet")
|
||||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||||
if syncErr != nil {
|
if syncErr != nil {
|
||||||
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
||||||
@@ -302,7 +303,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.logError("failed to install prerequisites", err)
|
g.logError("failed to install prerequisites", err)
|
||||||
@@ -503,14 +504,14 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
|
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
|
||||||
packageUseDir := "/etc/portage/package.use"
|
packageUseDir := "/etc/portage/package.use"
|
||||||
|
|
||||||
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
|
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("mkdir -p %s", packageUseDir))
|
fmt.Sprintf("mkdir -p %s", packageUseDir))
|
||||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||||
@@ -524,7 +525,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
|
|||||||
if checkExistingCmd.Run() == nil {
|
if checkExistingCmd.Run() == nil {
|
||||||
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
|
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
|
||||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||||
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
|
replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
|
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
|
||||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||||
@@ -532,7 +533,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendCmd := ExecSudoCommand(ctx, sudoPassword,
|
appendCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
|
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
|
||||||
|
|
||||||
output, err := appendCmd.CombinedOutput()
|
output, err := appendCmd.CombinedOutput()
|
||||||
@@ -557,7 +558,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enable GURU repository
|
// Enable GURU repository
|
||||||
enableCmd := ExecSudoCommand(ctx, sudoPassword,
|
enableCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
|
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
|
||||||
output, err := enableCmd.CombinedOutput()
|
output, err := enableCmd.CombinedOutput()
|
||||||
|
|
||||||
@@ -589,7 +590,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
|
|||||||
LogOutput: "Syncing GURU repository",
|
LogOutput: "Syncing GURU repository",
|
||||||
}
|
}
|
||||||
|
|
||||||
syncCmd := ExecSudoCommand(ctx, sudoPassword,
|
syncCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
|
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
|
||||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||||
|
|
||||||
@@ -622,7 +623,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
|||||||
|
|
||||||
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
|
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
|
||||||
|
|
||||||
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
|
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
|
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
|
||||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||||
@@ -636,7 +637,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
|||||||
if checkExistingCmd.Run() == nil {
|
if checkExistingCmd.Run() == nil {
|
||||||
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
|
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
|
||||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||||
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
|
replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
|
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
|
||||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||||
@@ -644,7 +645,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendCmd := ExecSudoCommand(ctx, sudoPassword,
|
appendCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
|
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
|
||||||
|
|
||||||
output, err := appendCmd.CombinedOutput()
|
output, err := appendCmd.CombinedOutput()
|
||||||
@@ -695,6 +696,6 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ManualPackageInstaller provides methods for installing packages from source
|
// ManualPackageInstaller provides methods for installing packages from source
|
||||||
@@ -143,7 +144,7 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
|
|||||||
CommandInfo: "sudo make install",
|
CommandInfo: "sudo make install",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
m.logError("failed to install dgop", err)
|
m.logError("failed to install dgop", err)
|
||||||
@@ -213,7 +214,7 @@ func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword s
|
|||||||
CommandInfo: "dpkg -i niri.deb",
|
CommandInfo: "dpkg -i niri.deb",
|
||||||
}
|
}
|
||||||
|
|
||||||
installDebCmd := ExecSudoCommand(ctx, sudoPassword,
|
installDebCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
|
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
|
||||||
|
|
||||||
output, err := installDebCmd.CombinedOutput()
|
output, err := installDebCmd.CombinedOutput()
|
||||||
@@ -324,7 +325,7 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
|
|||||||
CommandInfo: "sudo cmake --install build",
|
CommandInfo: "sudo cmake --install build",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
@@ -387,7 +388,7 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
|
|||||||
CommandInfo: "sudo make install",
|
CommandInfo: "sudo make install",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install Hyprland: %w", err)
|
return fmt.Errorf("failed to install Hyprland: %w", err)
|
||||||
@@ -453,7 +454,7 @@ func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPasswor
|
|||||||
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
|
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
installCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
|
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
return fmt.Errorf("failed to install Ghostty: %w", err)
|
||||||
@@ -492,16 +493,11 @@ func (m *ManualPackageInstaller) installMatugen(ctx context.Context, sudoPasswor
|
|||||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||||
}
|
}
|
||||||
|
|
||||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
|
||||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
|
||||||
if err := copyCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
|
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make it executable
|
if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
|
||||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
|
||||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
|
||||||
if err := chmodCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to make matugen executable: %w", err)
|
return fmt.Errorf("failed to make matugen executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,15 +642,11 @@ func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, s
|
|||||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||||
}
|
}
|
||||||
|
|
||||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
|
||||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
|
||||||
if err := copyCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
|
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
|
||||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
|
||||||
if err := chmodCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
|
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -249,7 +250,7 @@ func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPas
|
|||||||
|
|
||||||
args := []string{"zypper", "install", "-y"}
|
args := []string{"zypper", "install", "-y"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
o.logError("failed to install prerequisites", err)
|
o.logError("failed to install prerequisites", err)
|
||||||
@@ -485,7 +486,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
|
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword,
|
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("zypper addrepo -f %s", repoURL))
|
fmt.Sprintf("zypper addrepo -f %s", repoURL))
|
||||||
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
|
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
|
||||||
@@ -506,7 +507,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
|
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshCmd := ExecSudoCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
|
refreshCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
|
||||||
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
return fmt.Errorf("failed to refresh repositories: %w", err)
|
return fmt.Errorf("failed to refresh repositories: %w", err)
|
||||||
}
|
}
|
||||||
@@ -587,7 +588,7 @@ func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sud
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, alias := range aliases {
|
for _, alias := range aliases {
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias)))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", privesc.EscapeSingleQuotes(alias)))
|
||||||
repoOutput, err := cmd.CombinedOutput()
|
repoOutput, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
||||||
@@ -645,7 +646,7 @@ func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packag
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,7 +774,7 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
|
|||||||
CommandInfo: "sudo cmake --install build",
|
CommandInfo: "sudo cmake --install build",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
@@ -797,7 +798,7 @@ func (o *OpenSUSEDistribution) installRust(ctx context.Context, sudoPassword str
|
|||||||
CommandInfo: "sudo zypper install rustup",
|
CommandInfo: "sudo zypper install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "zypper install -y rustup")
|
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper install -y rustup")
|
||||||
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -177,7 +178,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Updating APT package lists",
|
LogOutput: "Updating APT package lists",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists: %w", err)
|
return fmt.Errorf("failed to update package lists: %w", err)
|
||||||
}
|
}
|
||||||
@@ -195,7 +196,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||||
if err := checkCmd.Run(); err != nil {
|
if err := checkCmd.Run(); err != nil {
|
||||||
// Not installed, install it
|
// Not installed, install it
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
cmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
||||||
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||||
}
|
}
|
||||||
@@ -211,7 +212,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Installing additional development tools",
|
LogOutput: "Installing additional development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
|
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
|
||||||
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
@@ -398,7 +399,7 @@ func (u *UbuntuDistribution) extractPackageNames(packages []PackageMapping) []st
|
|||||||
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
enabledRepos := make(map[string]bool)
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
installPPACmd := ExecSudoCommand(ctx, sudoPassword,
|
installPPACmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"apt-get install -y software-properties-common")
|
"apt-get install -y software-properties-common")
|
||||||
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
|
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
|
||||||
return fmt.Errorf("failed to install software-properties-common: %w", err)
|
return fmt.Errorf("failed to install software-properties-common: %w", err)
|
||||||
@@ -416,7 +417,7 @@ func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []Packa
|
|||||||
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
|
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword,
|
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
|
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
|
||||||
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
|
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
|
||||||
@@ -437,7 +438,7 @@ func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []Packa
|
|||||||
CommandInfo: "sudo apt-get update",
|
CommandInfo: "sudo apt-get update",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
|
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
|
||||||
}
|
}
|
||||||
@@ -504,7 +505,7 @@ func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []st
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,7 +592,7 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
args := []string{"apt-get", "install", "-y"}
|
args := []string{"apt-get", "install", "-y"}
|
||||||
args = append(args, depList...)
|
args = append(args, depList...)
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,7 +610,7 @@ func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword strin
|
|||||||
CommandInfo: "sudo apt-get install rustup",
|
CommandInfo: "sudo apt-get install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
||||||
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
@@ -649,7 +650,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
|
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
|
||||||
}
|
}
|
||||||
|
|
||||||
addPPACmd := ExecSudoCommand(ctx, sudoPassword,
|
addPPACmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"add-apt-repository -y ppa:longsleep/golang-backports")
|
"add-apt-repository -y ppa:longsleep/golang-backports")
|
||||||
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
|
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
|
||||||
return fmt.Errorf("failed to add Go PPA: %w", err)
|
return fmt.Errorf("failed to add Go PPA: %w", err)
|
||||||
@@ -664,7 +665,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get update",
|
CommandInfo: "sudo apt-get update",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
|
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
|
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
|
||||||
}
|
}
|
||||||
@@ -678,7 +679,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get install golang-go",
|
CommandInfo: "sudo apt-get install golang-go",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
||||||
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/sblinch/kdl-go"
|
"github.com/sblinch/kdl-go"
|
||||||
"github.com/sblinch/kdl-go/document"
|
"github.com/sblinch/kdl-go/document"
|
||||||
@@ -345,56 +346,17 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
switch config.Family {
|
switch config.Family {
|
||||||
case distros.FamilyArch:
|
case distros.FamilyArch:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm greetd")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"pacman -S --needed --noconfirm greetd")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "pacman", "-S", "--needed", "--noconfirm", "greetd")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyFedora:
|
case distros.FamilyFedora:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y greetd")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"dnf install -y greetd")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "greetd")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilySUSE:
|
case distros.FamilySUSE:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y greetd")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
case distros.FamilyUbuntu, distros.FamilyDebian:
|
||||||
"zypper install -y greetd")
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd")
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "greetd")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyUbuntu:
|
|
||||||
if sudoPassword != "" {
|
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"apt-get install -y greetd")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyDebian:
|
|
||||||
if sudoPassword != "" {
|
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"apt-get install -y greetd")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyGentoo:
|
case distros.FamilyGentoo:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"emerge --ask n sys-apps/greetd")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "emerge", "--ask", "n", "sys-apps/greetd")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyNix:
|
case distros.FamilyNix:
|
||||||
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
|
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported distribution family for automatic greetd installation: %s", config.Family)
|
return fmt.Errorf("unsupported distribution family for automatic greetd installation: %s", config.Family)
|
||||||
}
|
}
|
||||||
@@ -455,56 +417,56 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
|
|||||||
logFunc(fmt.Sprintf("Adding DankLinux OBS repository (%s)...", obsSlug))
|
logFunc(fmt.Sprintf("Adding DankLinux OBS repository (%s)...", obsSlug))
|
||||||
if _, err := exec.LookPath("gpg"); err != nil {
|
if _, err := exec.LookPath("gpg"); err != nil {
|
||||||
logFunc("Installing gnupg for OBS repository key import...")
|
logFunc("Installing gnupg for OBS repository key import...")
|
||||||
installGPGCmd := exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "gnupg")
|
installGPGCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y gnupg")
|
||||||
installGPGCmd.Stdout = os.Stdout
|
installGPGCmd.Stdout = os.Stdout
|
||||||
installGPGCmd.Stderr = os.Stderr
|
installGPGCmd.Stderr = os.Stderr
|
||||||
if err := installGPGCmd.Run(); err != nil {
|
if err := installGPGCmd.Run(); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Failed to install gnupg: %v", err))
|
logFunc(fmt.Sprintf("⚠ Failed to install gnupg: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mkdirCmd := exec.CommandContext(ctx, "sudo", "mkdir", "-p", "/etc/apt/keyrings")
|
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
||||||
mkdirCmd.Stdout = os.Stdout
|
mkdirCmd.Stdout = os.Stdout
|
||||||
mkdirCmd.Stderr = os.Stderr
|
mkdirCmd.Stderr = os.Stderr
|
||||||
mkdirCmd.Run()
|
mkdirCmd.Run()
|
||||||
addKeyCmd := exec.CommandContext(ctx, "bash", "-c",
|
addKeyCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf(`curl -fsSL %s | sudo gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg`, keyURL))
|
fmt.Sprintf(`bash -c "curl -fsSL %s | gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg"`, keyURL))
|
||||||
addKeyCmd.Stdout = os.Stdout
|
addKeyCmd.Stdout = os.Stdout
|
||||||
addKeyCmd.Stderr = os.Stderr
|
addKeyCmd.Stderr = os.Stderr
|
||||||
addKeyCmd.Run()
|
addKeyCmd.Run()
|
||||||
addRepoCmd := exec.CommandContext(ctx, "bash", "-c",
|
addRepoCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf(`echo '%s' | sudo tee /etc/apt/sources.list.d/danklinux.list`, repoLine))
|
fmt.Sprintf(`bash -c "echo '%s' > /etc/apt/sources.list.d/danklinux.list"`, repoLine))
|
||||||
addRepoCmd.Stdout = os.Stdout
|
addRepoCmd.Stdout = os.Stdout
|
||||||
addRepoCmd.Stderr = os.Stderr
|
addRepoCmd.Stderr = os.Stderr
|
||||||
addRepoCmd.Run()
|
addRepoCmd.Run()
|
||||||
exec.CommandContext(ctx, "sudo", "apt-get", "update").Run()
|
privesc.ExecCommand(ctx, sudoPassword, "apt-get update").Run()
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "dms-greeter")
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y dms-greeter")
|
||||||
case distros.FamilySUSE:
|
case distros.FamilySUSE:
|
||||||
repoURL := getOpenSUSEOBSRepoURL(osInfo)
|
repoURL := getOpenSUSEOBSRepoURL(osInfo)
|
||||||
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\nsudo zypper addrepo %s\nsudo zypper refresh && sudo zypper install dms-greeter", repoURL)
|
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\nsudo zypper addrepo %s\nsudo zypper refresh && sudo zypper install dms-greeter", repoURL)
|
||||||
logFunc("Adding DankLinux OBS repository...")
|
logFunc("Adding DankLinux OBS repository...")
|
||||||
addRepoCmd := exec.CommandContext(ctx, "sudo", "zypper", "addrepo", repoURL)
|
addRepoCmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("zypper addrepo %s", repoURL))
|
||||||
addRepoCmd.Stdout = os.Stdout
|
addRepoCmd.Stdout = os.Stdout
|
||||||
addRepoCmd.Stderr = os.Stderr
|
addRepoCmd.Stderr = os.Stderr
|
||||||
addRepoCmd.Run()
|
addRepoCmd.Run()
|
||||||
exec.CommandContext(ctx, "sudo", "zypper", "refresh").Run()
|
privesc.ExecCommand(ctx, sudoPassword, "zypper refresh").Run()
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "dms-greeter")
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y dms-greeter")
|
||||||
case distros.FamilyUbuntu:
|
case distros.FamilyUbuntu:
|
||||||
failHint = "⚠ dms-greeter install failed. Add PPA manually: sudo add-apt-repository ppa:avengemedia/danklinux && sudo apt-get update && sudo apt-get install -y dms-greeter"
|
failHint = "⚠ dms-greeter install failed. Add PPA manually: sudo add-apt-repository ppa:avengemedia/danklinux && sudo apt-get update && sudo apt-get install -y dms-greeter"
|
||||||
logFunc("Enabling PPA ppa:avengemedia/danklinux...")
|
logFunc("Enabling PPA ppa:avengemedia/danklinux...")
|
||||||
ppacmd := exec.CommandContext(ctx, "sudo", "add-apt-repository", "-y", "ppa:avengemedia/danklinux")
|
ppacmd := privesc.ExecCommand(ctx, sudoPassword, "add-apt-repository -y ppa:avengemedia/danklinux")
|
||||||
ppacmd.Stdout = os.Stdout
|
ppacmd.Stdout = os.Stdout
|
||||||
ppacmd.Stderr = os.Stderr
|
ppacmd.Stderr = os.Stderr
|
||||||
ppacmd.Run()
|
ppacmd.Run()
|
||||||
exec.CommandContext(ctx, "sudo", "apt-get", "update").Run()
|
privesc.ExecCommand(ctx, sudoPassword, "apt-get update").Run()
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "dms-greeter")
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y dms-greeter")
|
||||||
case distros.FamilyFedora:
|
case distros.FamilyFedora:
|
||||||
failHint = "⚠ dms-greeter install failed. Enable COPR manually: sudo dnf copr enable avengemedia/danklinux && sudo dnf install dms-greeter"
|
failHint = "⚠ dms-greeter install failed. Enable COPR manually: sudo dnf copr enable avengemedia/danklinux && sudo dnf install dms-greeter"
|
||||||
logFunc("Enabling COPR avengemedia/danklinux...")
|
logFunc("Enabling COPR avengemedia/danklinux...")
|
||||||
coprcmd := exec.CommandContext(ctx, "sudo", "dnf", "copr", "enable", "-y", "avengemedia/danklinux")
|
coprcmd := privesc.ExecCommand(ctx, sudoPassword, "dnf copr enable -y avengemedia/danklinux")
|
||||||
coprcmd.Stdout = os.Stdout
|
coprcmd.Stdout = os.Stdout
|
||||||
coprcmd.Stderr = os.Stderr
|
coprcmd.Stderr = os.Stderr
|
||||||
coprcmd.Run()
|
coprcmd.Run()
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "dms-greeter")
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y dms-greeter")
|
||||||
case distros.FamilyArch:
|
case distros.FamilyArch:
|
||||||
aurHelper := ""
|
aurHelper := ""
|
||||||
for _, helper := range []string{"paru", "yay"} {
|
for _, helper := range []string{"paru", "yay"} {
|
||||||
@@ -557,25 +519,25 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
|
|||||||
if info, err := os.Stat(wrapperDst); err == nil && !info.IsDir() {
|
if info, err := os.Stat(wrapperDst); err == nil && !info.IsDir() {
|
||||||
action = "Updated"
|
action = "Updated"
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil {
|
||||||
return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err)
|
return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err)
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf("✓ %s dms-greeter wrapper at %s", action, wrapperDst))
|
logFunc(fmt.Sprintf("✓ %s dms-greeter wrapper at %s", action, wrapperDst))
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "+x", wrapperDst); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "+x", wrapperDst); err != nil {
|
||||||
return fmt.Errorf("failed to make wrapper executable: %w", err)
|
return fmt.Errorf("failed to make wrapper executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
osInfo, err := distros.GetOSInfo()
|
osInfo, err := distros.GetOSInfo()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if config, exists := distros.Registry[osInfo.Distribution.ID]; exists && (config.Family == distros.FamilyFedora || config.Family == distros.FamilySUSE) {
|
if config, exists := distros.Registry[osInfo.Distribution.ID]; exists && (config.Family == distros.FamilyFedora || config.Family == distros.FamilySUSE) {
|
||||||
if err := runSudoCmd(sudoPassword, "semanage", "fcontext", "-a", "-t", "bin_t", wrapperDst); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "semanage", "fcontext", "-a", "-t", "bin_t", wrapperDst); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set SELinux fcontext: %v", err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set SELinux fcontext: %v", err))
|
||||||
} else {
|
} else {
|
||||||
logFunc("✓ Set SELinux fcontext for dms-greeter")
|
logFunc("✓ Set SELinux fcontext for dms-greeter")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "restorecon", "-v", wrapperDst); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "restorecon", "-v", wrapperDst); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context: %v", err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context: %v", err))
|
||||||
} else {
|
} else {
|
||||||
logFunc("✓ Restored SELinux context for dms-greeter")
|
logFunc("✓ Restored SELinux context for dms-greeter")
|
||||||
@@ -601,7 +563,7 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
|||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
return fmt.Errorf("failed to stat cache directory: %w", err)
|
return fmt.Errorf("failed to stat cache directory: %w", err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
created = true
|
created = true
|
||||||
@@ -613,17 +575,17 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
|||||||
daemonUser := DetectGreeterUser()
|
daemonUser := DetectGreeterUser()
|
||||||
preferredOwner := fmt.Sprintf("%s:%s", daemonUser, group)
|
preferredOwner := fmt.Sprintf("%s:%s", daemonUser, group)
|
||||||
owner := preferredOwner
|
owner := preferredOwner
|
||||||
if err := runSudoCmd(sudoPassword, "chown", owner, cacheDir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, cacheDir); err != nil {
|
||||||
// Some setups may not have a matching daemon user at this moment; fall back
|
// Some setups may not have a matching daemon user at this moment; fall back
|
||||||
// to root:<group> while still allowing group-writable greeter runtime access.
|
// to root:<group> while still allowing group-writable greeter runtime access.
|
||||||
fallbackOwner := fmt.Sprintf("root:%s", group)
|
fallbackOwner := fmt.Sprintf("root:%s", group)
|
||||||
if fallbackErr := runSudoCmd(sudoPassword, "chown", fallbackOwner, cacheDir); fallbackErr != nil {
|
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", fallbackOwner, cacheDir); fallbackErr != nil {
|
||||||
return fmt.Errorf("failed to set cache directory owner (preferred %s: %v; fallback %s: %w)", preferredOwner, err, fallbackOwner, fallbackErr)
|
return fmt.Errorf("failed to set cache directory owner (preferred %s: %v; fallback %s: %w)", preferredOwner, err, fallbackOwner, fallbackErr)
|
||||||
}
|
}
|
||||||
owner = fallbackOwner
|
owner = fallbackOwner
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "2770", cacheDir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "2770", cacheDir); err != nil {
|
||||||
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,13 +597,13 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
|||||||
filepath.Join(cacheDir, ".cache"),
|
filepath.Join(cacheDir, ".cache"),
|
||||||
}
|
}
|
||||||
for _, dir := range runtimeDirs {
|
for _, dir := range runtimeDirs {
|
||||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", dir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", dir); err != nil {
|
||||||
return fmt.Errorf("failed to create cache runtime directory %s: %w", dir, err)
|
return fmt.Errorf("failed to create cache runtime directory %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chown", owner, dir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, dir); err != nil {
|
||||||
return fmt.Errorf("failed to set owner for cache runtime directory %s: %w", dir, err)
|
return fmt.Errorf("failed to set owner for cache runtime directory %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "2770", dir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "2770", dir); err != nil {
|
||||||
return fmt.Errorf("failed to set permissions for cache runtime directory %s: %w", dir, err)
|
return fmt.Errorf("failed to set permissions for cache runtime directory %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -653,7 +615,7 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isSELinuxEnforcing() && utils.CommandExists("restorecon") {
|
if isSELinuxEnforcing() && utils.CommandExists("restorecon") {
|
||||||
if err := runSudoCmd(sudoPassword, "restorecon", "-Rv", cacheDir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "restorecon", "-Rv", cacheDir); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context for %s: %v", cacheDir, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context for %s: %v", cacheDir, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -678,13 +640,13 @@ func ensureGreeterMemoryCompatLink(logFunc func(string), sudoPassword, legacyPat
|
|||||||
info, err := os.Lstat(legacyPath)
|
info, err := os.Lstat(legacyPath)
|
||||||
if err == nil && info.Mode().IsRegular() {
|
if err == nil && info.Mode().IsRegular() {
|
||||||
if _, stateErr := os.Stat(statePath); os.IsNotExist(stateErr) {
|
if _, stateErr := os.Stat(statePath); os.IsNotExist(stateErr) {
|
||||||
if copyErr := runSudoCmd(sudoPassword, "cp", "-f", legacyPath, statePath); copyErr != nil {
|
if copyErr := privesc.Run(context.Background(), sudoPassword, "cp", "-f", legacyPath, statePath); copyErr != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to migrate legacy greeter memory file to %s: %v", statePath, copyErr))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to migrate legacy greeter memory file to %s: %v", statePath, copyErr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "ln", "-sfn", statePath, legacyPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sfn", statePath, legacyPath); err != nil {
|
||||||
return fmt.Errorf("failed to create greeter memory compatibility symlink %s -> %s: %w", legacyPath, statePath, err)
|
return fmt.Errorf("failed to create greeter memory compatibility symlink %s -> %s: %w", legacyPath, statePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,7 +673,7 @@ func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/apparmor.d"); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/apparmor.d"); err != nil {
|
||||||
return fmt.Errorf("failed to create /etc/apparmor.d: %w", err)
|
return fmt.Errorf("failed to create /etc/apparmor.d: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,15 +690,15 @@ func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
tmp.Close()
|
tmp.Close()
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "cp", tmpPath, appArmorProfileDest); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, appArmorProfileDest); err != nil {
|
||||||
return fmt.Errorf("failed to install AppArmor profile to %s: %w", appArmorProfileDest, err)
|
return fmt.Errorf("failed to install AppArmor profile to %s: %w", appArmorProfileDest, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "644", appArmorProfileDest); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", appArmorProfileDest); err != nil {
|
||||||
return fmt.Errorf("failed to set AppArmor profile permissions: %w", err)
|
return fmt.Errorf("failed to set AppArmor profile permissions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if utils.CommandExists("apparmor_parser") {
|
if utils.CommandExists("apparmor_parser") {
|
||||||
if err := runSudoCmd(sudoPassword, "apparmor_parser", "-r", appArmorProfileDest); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "apparmor_parser", "-r", appArmorProfileDest); err != nil {
|
||||||
logFunc(fmt.Sprintf(" ⚠ AppArmor profile installed but reload failed: %v", err))
|
logFunc(fmt.Sprintf(" ⚠ AppArmor profile installed but reload failed: %v", err))
|
||||||
logFunc(" Run: sudo apparmor_parser -r " + appArmorProfileDest)
|
logFunc(" Run: sudo apparmor_parser -r " + appArmorProfileDest)
|
||||||
} else {
|
} else {
|
||||||
@@ -783,10 +745,10 @@ func RemoveGreeterPamManagedBlock(logFunc func(string), sudoPassword string) err
|
|||||||
}
|
}
|
||||||
tmp.Close()
|
tmp.Close()
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
|
||||||
return fmt.Errorf("failed to write PAM config: %w", err)
|
return fmt.Errorf("failed to write PAM config: %w", err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "644", greetdPamPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", greetdPamPath); err != nil {
|
||||||
return fmt.Errorf("failed to set PAM config permissions: %w", err)
|
return fmt.Errorf("failed to set PAM config permissions: %w", err)
|
||||||
}
|
}
|
||||||
logFunc(" ✓ Removed DMS managed PAM block from " + greetdPamPath)
|
logFunc(" ✓ Removed DMS managed PAM block from " + greetdPamPath)
|
||||||
@@ -807,9 +769,9 @@ func UninstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if utils.CommandExists("apparmor_parser") {
|
if utils.CommandExists("apparmor_parser") {
|
||||||
_ = runSudoCmd(sudoPassword, "apparmor_parser", "--remove", appArmorProfileDest)
|
_ = privesc.Run(context.Background(), sudoPassword, "apparmor_parser", "--remove", appArmorProfileDest)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "rm", "-f", appArmorProfileDest); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", appArmorProfileDest); err != nil {
|
||||||
return fmt.Errorf("failed to remove AppArmor profile: %w", err)
|
return fmt.Errorf("failed to remove AppArmor profile: %w", err)
|
||||||
}
|
}
|
||||||
logFunc(" ✓ Removed DMS AppArmor profile")
|
logFunc(" ✓ Removed DMS AppArmor profile")
|
||||||
@@ -839,50 +801,17 @@ func EnsureACLInstalled(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
switch config.Family {
|
switch config.Family {
|
||||||
case distros.FamilyArch:
|
case distros.FamilyArch:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm acl")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm acl")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "pacman", "-S", "--needed", "--noconfirm", "acl")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyFedora:
|
case distros.FamilyFedora:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y acl")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "dnf install -y acl")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "acl")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilySUSE:
|
case distros.FamilySUSE:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y acl")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "zypper install -y acl")
|
case distros.FamilyUbuntu, distros.FamilyDebian:
|
||||||
} else {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y acl")
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "acl")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyUbuntu:
|
|
||||||
if sudoPassword != "" {
|
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "apt-get install -y acl")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "acl")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyDebian:
|
|
||||||
if sudoPassword != "" {
|
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "apt-get install -y acl")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "acl")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyGentoo:
|
case distros.FamilyGentoo:
|
||||||
if sudoPassword != "" {
|
installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-fs/acl")
|
||||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "emerge --ask n sys-fs/acl")
|
|
||||||
} else {
|
|
||||||
installCmd = exec.CommandContext(ctx, "sudo", "emerge", "--ask", "n", "sys-fs/acl")
|
|
||||||
}
|
|
||||||
|
|
||||||
case distros.FamilyNix:
|
case distros.FamilyNix:
|
||||||
return fmt.Errorf("on NixOS, please add pkgs.acl to your configuration.nix")
|
return fmt.Errorf("on NixOS, please add pkgs.acl to your configuration.nix")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported distribution family for automatic acl installation: %s", config.Family)
|
return fmt.Errorf("unsupported distribution family for automatic acl installation: %s", config.Family)
|
||||||
}
|
}
|
||||||
@@ -939,7 +868,7 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Group ACL covers daemon users regardless of username (e.g. greetd ≠ greeter on Fedora).
|
// Group ACL covers daemon users regardless of username (e.g. greetd ≠ greeter on Fedora).
|
||||||
if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("g:%s:rX", group), dir.path); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "setfacl", "-m", fmt.Sprintf("g:%s:rX", group), dir.path); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
|
||||||
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m g:%s:rX %s", group, dir.path))
|
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m g:%s:rX %s", group, dir.path))
|
||||||
continue
|
continue
|
||||||
@@ -996,7 +925,7 @@ func RemediateStaleACLs(logFunc func(string), sudoPassword string) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, user := range existingUsers {
|
for _, user := range existingUsers {
|
||||||
_ = runSudoCmd(sudoPassword, "setfacl", "-x", fmt.Sprintf("u:%s", user), dir)
|
_ = privesc.Run(context.Background(), sudoPassword, "setfacl", "-x", fmt.Sprintf("u:%s", user), dir)
|
||||||
cleaned = true
|
cleaned = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1036,7 +965,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
// Create the group if it doesn't exist yet (e.g. before greetd package is installed).
|
// Create the group if it doesn't exist yet (e.g. before greetd package is installed).
|
||||||
if !utils.HasGroup(group) {
|
if !utils.HasGroup(group) {
|
||||||
if err := runSudoCmd(sudoPassword, "groupadd", "-r", group); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "groupadd", "-r", group); err != nil {
|
||||||
return fmt.Errorf("failed to create %s group: %w", group, err)
|
return fmt.Errorf("failed to create %s group: %w", group, err)
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf("✓ Created system group %s", group))
|
logFunc(fmt.Sprintf("✓ Created system group %s", group))
|
||||||
@@ -1047,7 +976,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
if err == nil && strings.Contains(string(groupsOutput), group) {
|
if err == nil && strings.Contains(string(groupsOutput), group) {
|
||||||
logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group))
|
logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group))
|
||||||
} else {
|
} else {
|
||||||
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
|
||||||
return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err)
|
return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err)
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group))
|
logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group))
|
||||||
@@ -1062,7 +991,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
if strings.Contains(string(daemonGroupsOutput), group) {
|
if strings.Contains(string(daemonGroupsOutput), group) {
|
||||||
logFunc(fmt.Sprintf("✓ Greeter daemon user %s is already in %s group", daemonUser, group))
|
logFunc(fmt.Sprintf("✓ Greeter daemon user %s is already in %s group", daemonUser, group))
|
||||||
} else {
|
} else {
|
||||||
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, daemonUser); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", group, daemonUser); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: could not add %s to %s group: %v", daemonUser, group, err))
|
logFunc(fmt.Sprintf("⚠ Warning: could not add %s to %s group: %v", daemonUser, group, err))
|
||||||
} else {
|
} else {
|
||||||
logFunc(fmt.Sprintf("✓ Added greeter daemon user %s to %s group", daemonUser, group))
|
logFunc(fmt.Sprintf("✓ Added greeter daemon user %s to %s group", daemonUser, group))
|
||||||
@@ -1092,12 +1021,12 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "chgrp", "-R", group, dir.path); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chgrp", "-R", group, dir.path); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "-R", "g+rX", dir.path); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "-R", "g+rX", dir.path); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set permissions for %s: %v", dir.desc, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set permissions for %s: %v", dir.desc, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1309,8 +1238,8 @@ func syncGreeterColorSource(homeDir, cacheDir string, state greeterThemeSyncStat
|
|||||||
}
|
}
|
||||||
|
|
||||||
target := filepath.Join(cacheDir, "colors.json")
|
target := filepath.Join(cacheDir, "colors.json")
|
||||||
_ = runSudoCmd(sudoPassword, "rm", "-f", target)
|
_ = privesc.Run(context.Background(), sudoPassword, "rm", "-f", target)
|
||||||
if err := runSudoCmd(sudoPassword, "ln", "-sf", source, target); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sf", source, target); err != nil {
|
||||||
return fmt.Errorf("failed to create symlink for wallpaper based theming (%s -> %s): %w", target, source, err)
|
return fmt.Errorf("failed to create symlink for wallpaper based theming (%s -> %s): %w", target, source, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1362,9 +1291,9 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = runSudoCmd(sudoPassword, "rm", "-f", link.target)
|
_ = privesc.Run(context.Background(), sudoPassword, "rm", "-f", link.target)
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
|
||||||
return fmt.Errorf("failed to create symlink for %s (%s -> %s): %w", link.desc, link.target, link.source, err)
|
return fmt.Errorf("failed to create symlink for %s (%s -> %s): %w", link.desc, link.target, link.source, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1406,13 +1335,13 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
|||||||
func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPassword string, state greeterThemeSyncState) error {
|
func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPassword string, state greeterThemeSyncState) error {
|
||||||
destPath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
destPath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
||||||
if state.ResolvedGreeterWallpaperPath == "" {
|
if state.ResolvedGreeterWallpaperPath == "" {
|
||||||
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", destPath); err != nil {
|
||||||
return fmt.Errorf("failed to clear override file %s: %w", destPath, err)
|
return fmt.Errorf("failed to clear override file %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
logFunc("✓ Cleared greeter wallpaper override")
|
logFunc("✓ Cleared greeter wallpaper override")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", destPath); err != nil {
|
||||||
return fmt.Errorf("failed to remove old override file %s: %w", destPath, err)
|
return fmt.Errorf("failed to remove old override file %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
src := state.ResolvedGreeterWallpaperPath
|
src := state.ResolvedGreeterWallpaperPath
|
||||||
@@ -1423,17 +1352,17 @@ func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPas
|
|||||||
if st.IsDir() {
|
if st.IsDir() {
|
||||||
return fmt.Errorf("configured greeter wallpaper path points to a directory: %s", src)
|
return fmt.Errorf("configured greeter wallpaper path points to a directory: %s", src)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "cp", src, destPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", src, destPath); err != nil {
|
||||||
return fmt.Errorf("failed to copy override wallpaper to %s: %w", destPath, err)
|
return fmt.Errorf("failed to copy override wallpaper to %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
greeterGroup := DetectGreeterGroup()
|
greeterGroup := DetectGreeterGroup()
|
||||||
daemonUser := DetectGreeterUser()
|
daemonUser := DetectGreeterUser()
|
||||||
if err := runSudoCmd(sudoPassword, "chown", daemonUser+":"+greeterGroup, destPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chown", daemonUser+":"+greeterGroup, destPath); err != nil {
|
||||||
if fallbackErr := runSudoCmd(sudoPassword, "chown", "root:"+greeterGroup, destPath); fallbackErr != nil {
|
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", "root:"+greeterGroup, destPath); fallbackErr != nil {
|
||||||
return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err)
|
return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", destPath); err != nil {
|
||||||
return fmt.Errorf("failed to set override permissions on %s: %w", destPath, err)
|
return fmt.Errorf("failed to set override permissions on %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
logFunc("✓ Synced greeter wallpaper override")
|
logFunc("✓ Synced greeter wallpaper override")
|
||||||
@@ -1798,10 +1727,10 @@ func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword str
|
|||||||
if err := tmpFile.Close(); err != nil {
|
if err := tmpFile.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
|
||||||
return fmt.Errorf("failed to install updated PAM config at %s: %w", greetdPamPath, err)
|
return fmt.Errorf("failed to install updated PAM config at %s: %w", greetdPamPath, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "644", greetdPamPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", greetdPamPath); err != nil {
|
||||||
return fmt.Errorf("failed to set permissions on %s: %w", greetdPamPath, err)
|
return fmt.Errorf("failed to set permissions on %s: %w", greetdPamPath, err)
|
||||||
}
|
}
|
||||||
if wantFprint || wantU2f {
|
if wantFprint || wantU2f {
|
||||||
@@ -1860,13 +1789,13 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
greeterDir := "/etc/greetd/niri"
|
greeterDir := "/etc/greetd/niri"
|
||||||
greeterGroup := DetectGreeterGroup()
|
greeterGroup := DetectGreeterGroup()
|
||||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", greeterDir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", greeterDir); err != nil {
|
||||||
return fmt.Errorf("failed to create greetd niri directory: %w", err)
|
return fmt.Errorf("failed to create greetd niri directory: %w", err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil {
|
||||||
return fmt.Errorf("failed to set greetd niri directory ownership: %w", err)
|
return fmt.Errorf("failed to set greetd niri directory ownership: %w", err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "755", greeterDir); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "755", greeterDir); err != nil {
|
||||||
return fmt.Errorf("failed to set greetd niri directory permissions: %w", err)
|
return fmt.Errorf("failed to set greetd niri directory permissions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1888,7 +1817,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
|||||||
if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil {
|
if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil {
|
||||||
return fmt.Errorf("failed to backup %s: %w", dmsPath, err)
|
return fmt.Errorf("failed to backup %s: %w", dmsPath, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil {
|
||||||
return fmt.Errorf("failed to install greetd niri dms config: %w", err)
|
return fmt.Errorf("failed to install greetd niri dms config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1911,7 +1840,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
|||||||
if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil {
|
if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil {
|
||||||
return fmt.Errorf("failed to backup %s: %w", mainPath, err)
|
return fmt.Errorf("failed to backup %s: %w", mainPath, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil {
|
||||||
return fmt.Errorf("failed to install greetd niri main config: %w", err)
|
return fmt.Errorf("failed to install greetd niri main config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1987,7 +1916,7 @@ func ensureGreetdNiriConfig(logFunc func(string), sudoPassword string, niriConfi
|
|||||||
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "mv", tmpFile.Name(), configPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "mv", tmpFile.Name(), configPath); err != nil {
|
||||||
return fmt.Errorf("failed to update greetd config: %w", err)
|
return fmt.Errorf("failed to update greetd config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2003,10 +1932,10 @@ func backupFileIfExists(sudoPassword string, path string, suffix string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405"))
|
backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405"))
|
||||||
if err := runSudoCmd(sudoPassword, "cp", path, backupPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "cp", path, backupPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return runSudoCmd(sudoPassword, "chmod", "644", backupPath)
|
return privesc.Run(context.Background(), sudoPassword, "chmod", "644", backupPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *niriGreeterSync) processFile(filePath string) error {
|
func (s *niriGreeterSync) processFile(filePath string) error {
|
||||||
@@ -2244,11 +2173,11 @@ vt = 1
|
|||||||
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil {
|
||||||
return fmt.Errorf("failed to create /etc/greetd: %w", err)
|
return fmt.Errorf("failed to create /etc/greetd: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil {
|
||||||
return fmt.Errorf("failed to install greetd config: %w", err)
|
return fmt.Errorf("failed to install greetd config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2352,27 +2281,6 @@ func getOpenSUSEOBSRepoURL(osInfo *distros.OSInfo) string {
|
|||||||
return fmt.Sprintf("%s/%s/home:AvengeMedia:danklinux.repo", base, slug)
|
return fmt.Sprintf("%s/%s/home:AvengeMedia:danklinux.repo", base, slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSudoCmd(sudoPassword string, command string, args ...string) error {
|
|
||||||
var cmd *exec.Cmd
|
|
||||||
|
|
||||||
if sudoPassword != "" {
|
|
||||||
fullArgs := append([]string{command}, args...)
|
|
||||||
quotedArgs := make([]string, len(fullArgs))
|
|
||||||
for i, arg := range fullArgs {
|
|
||||||
quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
|
|
||||||
}
|
|
||||||
cmdStr := strings.Join(quotedArgs, " ")
|
|
||||||
|
|
||||||
cmd = distros.ExecSudoCommand(context.Background(), sudoPassword, cmdStr)
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command("sudo", append([]string{command}, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkSystemdEnabled(service string) (string, error) {
|
func checkSystemdEnabled(service string) (string, error) {
|
||||||
cmd := exec.Command("systemctl", "is-enabled", service)
|
cmd := exec.Command("systemctl", "is-enabled", service)
|
||||||
output, _ := cmd.Output()
|
output, _ := cmd.Output()
|
||||||
@@ -2389,7 +2297,7 @@ func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)
|
|||||||
switch state {
|
switch state {
|
||||||
case "enabled", "enabled-runtime", "static", "indirect", "alias":
|
case "enabled", "enabled-runtime", "static", "indirect", "alias":
|
||||||
logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm))
|
logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm))
|
||||||
if err := runSudoCmd(sudoPassword, "systemctl", "disable", dm); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "disable", dm); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err))
|
||||||
} else {
|
} else {
|
||||||
logFunc(fmt.Sprintf("✓ Disabled %s", dm))
|
logFunc(fmt.Sprintf("✓ Disabled %s", dm))
|
||||||
@@ -2410,13 +2318,13 @@ func EnableGreetd(sudoPassword string, logFunc func(string)) error {
|
|||||||
}
|
}
|
||||||
if state == "masked" || state == "masked-runtime" {
|
if state == "masked" || state == "masked-runtime" {
|
||||||
logFunc(" Unmasking greetd...")
|
logFunc(" Unmasking greetd...")
|
||||||
if err := runSudoCmd(sudoPassword, "systemctl", "unmask", "greetd"); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "unmask", "greetd"); err != nil {
|
||||||
return fmt.Errorf("failed to unmask greetd: %w", err)
|
return fmt.Errorf("failed to unmask greetd: %w", err)
|
||||||
}
|
}
|
||||||
logFunc(" ✓ Unmasked greetd")
|
logFunc(" ✓ Unmasked greetd")
|
||||||
}
|
}
|
||||||
logFunc(" Enabling greetd service (--force)...")
|
logFunc(" Enabling greetd service (--force)...")
|
||||||
if err := runSudoCmd(sudoPassword, "systemctl", "enable", "--force", "greetd"); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "enable", "--force", "greetd"); err != nil {
|
||||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||||
}
|
}
|
||||||
logFunc("✓ greetd enabled")
|
logFunc("✓ greetd enabled")
|
||||||
@@ -2436,7 +2344,7 @@ func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf(" Setting default target to graphical.target (was: %s)...", current))
|
logFunc(fmt.Sprintf(" Setting default target to graphical.target (was: %s)...", current))
|
||||||
if err := runSudoCmd(sudoPassword, "systemctl", "set-default", "graphical.target"); err != nil {
|
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "set-default", "graphical.target"); err != nil {
|
||||||
return fmt.Errorf("failed to set graphical target: %w", err)
|
return fmt.Errorf("failed to set graphical target: %w", err)
|
||||||
}
|
}
|
||||||
logFunc("✓ Default target set to graphical.target")
|
logFunc("✓ Default target set to graphical.target")
|
||||||
|
|||||||
385
core/internal/privesc/privesc.go
Normal file
385
core/internal/privesc/privesc.go
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
package privesc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tool identifies a privilege-escalation binary.
|
||||||
|
type Tool string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ToolSudo Tool = "sudo"
|
||||||
|
ToolDoas Tool = "doas"
|
||||||
|
ToolRun0 Tool = "run0"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnvVar selects a specific tool when set to one of: sudo, doas, run0.
|
||||||
|
const EnvVar = "DMS_PRIVESC"
|
||||||
|
|
||||||
|
var detectionOrder = []Tool{ToolSudo, ToolDoas, ToolRun0}
|
||||||
|
|
||||||
|
var (
|
||||||
|
detectOnce sync.Once
|
||||||
|
detected Tool
|
||||||
|
detectErr error
|
||||||
|
userSelected bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// Detect returns the tool that should be used for privilege escalation.
|
||||||
|
// The result is cached after the first call.
|
||||||
|
func Detect() (Tool, error) {
|
||||||
|
detectOnce.Do(func() {
|
||||||
|
detected, detectErr = detectTool()
|
||||||
|
})
|
||||||
|
return detected, detectErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetForTesting clears cached detection state.
|
||||||
|
func ResetForTesting() {
|
||||||
|
detectOnce = sync.Once{}
|
||||||
|
detected = ""
|
||||||
|
detectErr = nil
|
||||||
|
userSelected = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AvailableTools returns the set of supported tools that are installed on
|
||||||
|
// PATH, in detection-precedence order.
|
||||||
|
func AvailableTools() []Tool {
|
||||||
|
var out []Tool
|
||||||
|
for _, t := range detectionOrder {
|
||||||
|
if t.Available() {
|
||||||
|
out = append(out, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnvOverride returns the tool selected by the $DMS_PRIVESC env var (if any)
|
||||||
|
// along with ok=true when the variable is set. An empty or unset variable
|
||||||
|
// returns ok=false.
|
||||||
|
func EnvOverride() (Tool, bool) {
|
||||||
|
v := strings.ToLower(strings.TrimSpace(os.Getenv(EnvVar)))
|
||||||
|
if v == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return Tool(v), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTool forces the detected tool to t, bypassing autodetection. Intended
|
||||||
|
// for use after the caller has prompted the user for a selection.
|
||||||
|
func SetTool(t Tool) error {
|
||||||
|
if !t.Available() {
|
||||||
|
return fmt.Errorf("%q is not installed", t.Name())
|
||||||
|
}
|
||||||
|
detectOnce = sync.Once{}
|
||||||
|
detectOnce.Do(func() {
|
||||||
|
detected = t
|
||||||
|
detectErr = nil
|
||||||
|
})
|
||||||
|
userSelected = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectTool() (Tool, error) {
|
||||||
|
switch override := strings.ToLower(strings.TrimSpace(os.Getenv(EnvVar))); override {
|
||||||
|
case "":
|
||||||
|
// fall through to autodetect
|
||||||
|
case string(ToolSudo), string(ToolDoas), string(ToolRun0):
|
||||||
|
t := Tool(override)
|
||||||
|
if !t.Available() {
|
||||||
|
return "", fmt.Errorf("%s=%s but %q is not installed", EnvVar, override, t.Name())
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("invalid %s=%q: must be one of sudo, doas, run0", EnvVar, override)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range detectionOrder {
|
||||||
|
if t.Available() {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("no supported privilege escalation tool found (tried: sudo, doas, run0)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the binary name.
|
||||||
|
func (t Tool) Name() string { return string(t) }
|
||||||
|
|
||||||
|
// Available reports whether this tool's binary is on PATH.
|
||||||
|
func (t Tool) Available() bool {
|
||||||
|
if t == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := exec.LookPath(string(t))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsStdinPassword reports whether the tool can accept a password via
|
||||||
|
// stdin. Only sudo (-S) supports this.
|
||||||
|
func (t Tool) SupportsStdinPassword() bool {
|
||||||
|
return t == ToolSudo
|
||||||
|
}
|
||||||
|
|
||||||
|
// EscapeSingleQuotes escapes single quotes for safe inclusion inside a
|
||||||
|
// bash single-quoted string.
|
||||||
|
func EscapeSingleQuotes(s string) string {
|
||||||
|
return strings.ReplaceAll(s, "'", "'\\''")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeCommand returns a bash command string that runs `command` with the
|
||||||
|
// detected tool. When the tool supports stdin passwords and password is
|
||||||
|
// non-empty, the password is piped in. Otherwise the tool is invoked with
|
||||||
|
// no non-interactive flag so that an interactive TTY prompt is still
|
||||||
|
// possible for CLI callers.
|
||||||
|
//
|
||||||
|
// If detection fails, the returned shell string exits 1 with an error
|
||||||
|
// message so callers that treat the *exec.Cmd as infallible still fail
|
||||||
|
// deterministically.
|
||||||
|
func MakeCommand(password, command string) string {
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return failingShell(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case ToolSudo:
|
||||||
|
if password != "" {
|
||||||
|
return fmt.Sprintf("echo '%s' | sudo -S %s", EscapeSingleQuotes(password), command)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("sudo %s", command)
|
||||||
|
case ToolDoas:
|
||||||
|
return fmt.Sprintf("doas sh -c '%s'", EscapeSingleQuotes(command))
|
||||||
|
case ToolRun0:
|
||||||
|
return fmt.Sprintf("run0 sh -c '%s'", EscapeSingleQuotes(command))
|
||||||
|
default:
|
||||||
|
return failingShell(fmt.Errorf("unsupported privilege tool: %q", t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecCommand builds an exec.Cmd that runs `command` as root via the
|
||||||
|
// detected tool. Detection errors surface at Run() time as a failing
|
||||||
|
// command writing a clear error to stderr.
|
||||||
|
func ExecCommand(ctx context.Context, password, command string) *exec.Cmd {
|
||||||
|
return exec.CommandContext(ctx, "bash", "-c", MakeCommand(password, command))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecArgv builds an exec.Cmd that runs argv as root via the detected tool.
|
||||||
|
// No stdin password is supplied; callers relying on non-interactive success
|
||||||
|
// should ensure cached credentials are present (see CheckCached).
|
||||||
|
func ExecArgv(ctx context.Context, argv ...string) *exec.Cmd {
|
||||||
|
if len(argv) == 0 {
|
||||||
|
return exec.CommandContext(ctx, "bash", "-c", failingShell(fmt.Errorf("privesc.ExecArgv: argv must not be empty")))
|
||||||
|
}
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return exec.CommandContext(ctx, "bash", "-c", failingShell(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case ToolSudo, ToolDoas:
|
||||||
|
return exec.CommandContext(ctx, string(t), argv...)
|
||||||
|
case ToolRun0:
|
||||||
|
return exec.CommandContext(ctx, "run0", argv...)
|
||||||
|
default:
|
||||||
|
return exec.CommandContext(ctx, "bash", "-c", failingShell(fmt.Errorf("unsupported privilege tool: %q", t)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func failingShell(err error) string {
|
||||||
|
return fmt.Sprintf("printf 'privesc: %%s\\n' '%s' >&2; exit 1", EscapeSingleQuotes(err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckCached runs a non-interactive credential probe. Returns nil if the
|
||||||
|
// tool will run commands without prompting (cached credentials, nopass, or
|
||||||
|
// polkit rule).
|
||||||
|
func CheckCached(ctx context.Context) error {
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch t {
|
||||||
|
case ToolSudo:
|
||||||
|
cmd = exec.CommandContext(ctx, "sudo", "-n", "true")
|
||||||
|
case ToolDoas:
|
||||||
|
cmd = exec.CommandContext(ctx, "doas", "-n", "true")
|
||||||
|
case ToolRun0:
|
||||||
|
cmd = exec.CommandContext(ctx, "run0", "--no-ask-password", "true")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported privilege tool: %q", t)
|
||||||
|
}
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearCache invalidates any cached credentials. No-op for tools that do
|
||||||
|
// not expose a cache-clear operation.
|
||||||
|
func ClearCache(ctx context.Context) error {
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch t {
|
||||||
|
case ToolSudo:
|
||||||
|
return exec.CommandContext(ctx, "sudo", "-k").Run()
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateWithAskpass validates cached credentials using an askpass helper
|
||||||
|
// script. Only sudo supports this mechanism; the TUI uses it to trigger
|
||||||
|
// fingerprint authentication via PAM.
|
||||||
|
func ValidateWithAskpass(ctx context.Context, askpassScript string) error {
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t != ToolSudo {
|
||||||
|
return fmt.Errorf("askpass validation requires sudo (detected: %s)", t)
|
||||||
|
}
|
||||||
|
cmd := exec.CommandContext(ctx, "sudo", "-A", "-v")
|
||||||
|
cmd.Env = append(os.Environ(), fmt.Sprintf("SUDO_ASKPASS=%s", askpassScript))
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePassword validates the given password. Only sudo supports this
|
||||||
|
// (via `sudo -S -v`); for other tools the caller should fall back to
|
||||||
|
// CheckCached.
|
||||||
|
func ValidatePassword(ctx context.Context, password string) error {
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t != ToolSudo {
|
||||||
|
return fmt.Errorf("password validation requires sudo (detected: %s)", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(stdin, "%s\n", password); err != nil {
|
||||||
|
stdin.Close()
|
||||||
|
_ = cmd.Wait()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stdin.Close()
|
||||||
|
return cmd.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuoteArgsForShell wraps each argv element in single quotes so the result
|
||||||
|
// can be safely passed to bash -c.
|
||||||
|
func QuoteArgsForShell(argv []string) string {
|
||||||
|
parts := make([]string, len(argv))
|
||||||
|
for i, a := range argv {
|
||||||
|
parts[i] = "'" + EscapeSingleQuotes(a) + "'"
|
||||||
|
}
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run invokes argv with privilege escalation. When the tool supports stdin
|
||||||
|
// passwords and password is non-empty, the password is piped in. Otherwise
|
||||||
|
// argv is invoked directly, which may prompt on a TTY.
|
||||||
|
// Stdout and Stderr are inherited from the current process.
|
||||||
|
func Run(ctx context.Context, password string, argv ...string) error {
|
||||||
|
if len(argv) == 0 {
|
||||||
|
return fmt.Errorf("privesc.Run: argv must not be empty")
|
||||||
|
}
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch {
|
||||||
|
case t == ToolSudo && password != "":
|
||||||
|
cmd = ExecCommand(ctx, password, QuoteArgsForShell(argv))
|
||||||
|
default:
|
||||||
|
cmd = ExecArgv(ctx, argv...)
|
||||||
|
}
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// stdinIsTTY reports whether stdin is a character device (interactive
|
||||||
|
// terminal) rather than a pipe or file.
|
||||||
|
func stdinIsTTY() bool {
|
||||||
|
fi, err := os.Stdin.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (fi.Mode() & os.ModeCharDevice) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// PromptCLI interactively prompts the user to pick a privilege tool when more
|
||||||
|
// than one is installed and $DMS_PRIVESC is not set. If stdin is not a TTY,
|
||||||
|
// or only one tool is available, or the env var is set, the detected tool is
|
||||||
|
// returned without any prompt.
|
||||||
|
//
|
||||||
|
// The prompt is written to out (typically os.Stdout/os.Stderr) and input is
|
||||||
|
// read from in. EOF or empty input selects the first option.
|
||||||
|
func PromptCLI(out io.Writer, in io.Reader) (Tool, error) {
|
||||||
|
if userSelected {
|
||||||
|
return Detect()
|
||||||
|
}
|
||||||
|
if _, envSet := EnvOverride(); envSet {
|
||||||
|
return Detect()
|
||||||
|
}
|
||||||
|
|
||||||
|
tools := AvailableTools()
|
||||||
|
switch len(tools) {
|
||||||
|
case 0:
|
||||||
|
return "", fmt.Errorf("no supported privilege tool (sudo/doas/run0) found on PATH")
|
||||||
|
case 1:
|
||||||
|
if err := SetTool(tools[0]); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return tools[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stdinIsTTY() {
|
||||||
|
return Detect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(out, "Multiple privilege escalation tools detected:")
|
||||||
|
for i, t := range tools {
|
||||||
|
fmt.Fprintf(out, " [%d] %s\n", i+1, t.Name())
|
||||||
|
}
|
||||||
|
fmt.Fprintf(out, "Choose one [1-%d] (default 1, or set %s=<tool> to skip): ", len(tools), EnvVar)
|
||||||
|
|
||||||
|
reader := bufio.NewReader(in)
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return "", fmt.Errorf("failed to read selection: %w", err)
|
||||||
|
}
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
|
||||||
|
idx := 1
|
||||||
|
if line != "" {
|
||||||
|
n, convErr := strconv.Atoi(line)
|
||||||
|
if convErr != nil || n < 1 || n > len(tools) {
|
||||||
|
return "", fmt.Errorf("invalid selection %q", line)
|
||||||
|
}
|
||||||
|
idx = n
|
||||||
|
}
|
||||||
|
|
||||||
|
chosen := tools[idx-1]
|
||||||
|
if err := SetTool(chosen); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return chosen, nil
|
||||||
|
}
|
||||||
@@ -391,7 +391,7 @@ func (m *Manager) Close() {
|
|||||||
|
|
||||||
func InitializeManager() (*Manager, error) {
|
func InitializeManager() (*Manager, error) {
|
||||||
if os.Getuid() != 0 && !hasInputGroupAccess() {
|
if os.Getuid() != 0 && !hasInputGroupAccess() {
|
||||||
return nil, fmt.Errorf("insufficient permissions to access input devices")
|
return nil, fmt.Errorf("insufficient permissions to access input devices. Add your user to the 'input' group: `sudo usermod -a -G input $USER` or run `dms setup`")
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewManager()
|
return NewManager()
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ func (m *Manager) claimScreensaverName(handler *screensaverHandler, name, iface
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if reply != dbus.RequestNameReplyPrimaryOwner {
|
if reply != dbus.RequestNameReplyPrimaryOwner {
|
||||||
log.Warnf("Screensaver name %s already owned by another process", name)
|
log.Infof("Screensaver name %s already owned by another process (e.g. hypridle/swayidle)", name)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil {
|
if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package tui
|
|||||||
import (
|
import (
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/charmbracelet/bubbles/spinner"
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
@@ -42,6 +43,9 @@ type Model struct {
|
|||||||
sudoPassword string
|
sudoPassword string
|
||||||
existingConfigs []ExistingConfigInfo
|
existingConfigs []ExistingConfigInfo
|
||||||
fingerprintFailed bool
|
fingerprintFailed bool
|
||||||
|
|
||||||
|
availablePrivesc []privesc.Tool
|
||||||
|
selectedPrivesc int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel(version string, logFilePath string) Model {
|
func NewModel(version string, logFilePath string) Model {
|
||||||
@@ -147,6 +151,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m.updateGentooUseFlagsState(msg)
|
return m.updateGentooUseFlagsState(msg)
|
||||||
case StateGentooGCCCheck:
|
case StateGentooGCCCheck:
|
||||||
return m.updateGentooGCCCheckState(msg)
|
return m.updateGentooGCCCheckState(msg)
|
||||||
|
case StateSelectPrivesc:
|
||||||
|
return m.updateSelectPrivescState(msg)
|
||||||
case StateAuthMethodChoice:
|
case StateAuthMethodChoice:
|
||||||
return m.updateAuthMethodChoiceState(msg)
|
return m.updateAuthMethodChoiceState(msg)
|
||||||
case StateFingerprintAuth:
|
case StateFingerprintAuth:
|
||||||
@@ -189,6 +195,8 @@ func (m Model) View() string {
|
|||||||
return m.viewGentooUseFlags()
|
return m.viewGentooUseFlags()
|
||||||
case StateGentooGCCCheck:
|
case StateGentooGCCCheck:
|
||||||
return m.viewGentooGCCCheck()
|
return m.viewGentooGCCCheck()
|
||||||
|
case StateSelectPrivesc:
|
||||||
|
return m.viewSelectPrivesc()
|
||||||
case StateAuthMethodChoice:
|
case StateAuthMethodChoice:
|
||||||
return m.viewAuthMethodChoice()
|
return m.viewAuthMethodChoice()
|
||||||
case StateFingerprintAuth:
|
case StateFingerprintAuth:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const (
|
|||||||
StateDependencyReview
|
StateDependencyReview
|
||||||
StateGentooUseFlags
|
StateGentooUseFlags
|
||||||
StateGentooGCCCheck
|
StateGentooGCCCheck
|
||||||
|
StateSelectPrivesc
|
||||||
StateAuthMethodChoice
|
StateAuthMethodChoice
|
||||||
StateFingerprintAuth
|
StateFingerprintAuth
|
||||||
StatePasswordPrompt
|
StatePasswordPrompt
|
||||||
|
|||||||
@@ -180,16 +180,7 @@ func (m Model) updateDependencyReviewState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check if fingerprint is enabled
|
return m.enterAuthPhase()
|
||||||
if checkFingerprintEnabled() {
|
|
||||||
m.state = StateAuthMethodChoice
|
|
||||||
m.selectedConfig = 0 // Default to fingerprint
|
|
||||||
return m, nil
|
|
||||||
} else {
|
|
||||||
m.state = StatePasswordPrompt
|
|
||||||
m.passwordInput.Focus()
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
case "esc":
|
case "esc":
|
||||||
m.state = StateSelectWindowManager
|
m.state = StateSelectWindowManager
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|||||||
@@ -56,14 +56,7 @@ func (m Model) updateGentooUseFlagsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.state = StateGentooGCCCheck
|
m.state = StateGentooGCCCheck
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
if checkFingerprintEnabled() {
|
return m.enterAuthPhase()
|
||||||
m.state = StateAuthMethodChoice
|
|
||||||
m.selectedConfig = 0
|
|
||||||
} else {
|
|
||||||
m.state = StatePasswordPrompt
|
|
||||||
m.passwordInput.Focus()
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||||
@@ -75,14 +68,7 @@ func (m Model) updateGentooUseFlagsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if m.selectedWM == 1 {
|
if m.selectedWM == 1 {
|
||||||
return m, m.checkGCCVersion()
|
return m, m.checkGCCVersion()
|
||||||
}
|
}
|
||||||
if checkFingerprintEnabled() {
|
return m.enterAuthPhase()
|
||||||
m.state = StateAuthMethodChoice
|
|
||||||
m.selectedConfig = 0
|
|
||||||
} else {
|
|
||||||
m.state = StatePasswordPrompt
|
|
||||||
m.passwordInput.Focus()
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case "esc":
|
case "esc":
|
||||||
m.state = StateDependencyReview
|
m.state = StateDependencyReview
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -274,8 +275,7 @@ func (m Model) delayThenReturn() tea.Cmd {
|
|||||||
|
|
||||||
func (m Model) tryFingerprint() tea.Cmd {
|
func (m Model) tryFingerprint() tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
clearCmd := exec.Command("sudo", "-k")
|
_ = privesc.ClearCache(context.Background())
|
||||||
clearCmd.Run()
|
|
||||||
|
|
||||||
tmpDir := os.TempDir()
|
tmpDir := os.TempDir()
|
||||||
askpassScript := filepath.Join(tmpDir, fmt.Sprintf("danklinux-fp-%d.sh", time.Now().UnixNano()))
|
askpassScript := filepath.Join(tmpDir, fmt.Sprintf("danklinux-fp-%d.sh", time.Now().UnixNano()))
|
||||||
@@ -289,15 +289,9 @@ func (m Model) tryFingerprint() tea.Cmd {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "sudo", "-A", "-v")
|
if err := privesc.ValidateWithAskpass(ctx, askpassScript); err != nil {
|
||||||
cmd.Env = append(os.Environ(), fmt.Sprintf("SUDO_ASKPASS=%s", askpassScript))
|
|
||||||
|
|
||||||
err := cmd.Run()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
return passwordValidMsg{password: "", valid: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
return passwordValidMsg{password: "", valid: true}
|
return passwordValidMsg{password: "", valid: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,32 +301,9 @@ func (m Model) validatePassword(password string) tea.Cmd {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
if err := privesc.ValidatePassword(ctx, password); err != nil {
|
||||||
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
return passwordValidMsg{password: "", valid: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = fmt.Fprintf(stdin, "%s\n", password)
|
|
||||||
stdin.Close()
|
|
||||||
if err != nil {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = cmd.Wait()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if ctx.Err() == context.DeadlineExceeded {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
return passwordValidMsg{password: password, valid: true}
|
return passwordValidMsg{password: password, valid: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
133
core/internal/tui/views_privesc.go
Normal file
133
core/internal/tui/views_privesc.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m Model) viewSelectPrivesc() string {
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
b.WriteString(m.renderBanner())
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(m.styles.Title.Render("Privilege Escalation Tool"))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
b.WriteString(m.styles.Normal.Render("Multiple privilege tools are available. Choose one for installation:"))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
|
for i, t := range m.availablePrivesc {
|
||||||
|
label := fmt.Sprintf("%s — %s", t.Name(), privescToolDescription(t))
|
||||||
|
switch i {
|
||||||
|
case m.selectedPrivesc:
|
||||||
|
b.WriteString(m.styles.SelectedOption.Render("▶ " + label))
|
||||||
|
default:
|
||||||
|
b.WriteString(m.styles.Normal.Render(" " + label))
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(m.styles.Subtle.Render(fmt.Sprintf("Set %s=<tool> to skip this prompt in future runs.", privesc.EnvVar)))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
b.WriteString(m.styles.Subtle.Render("↑/↓: Navigate, Enter: Select, Esc: Back"))
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) updateSelectPrivescState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
keyMsg, ok := msg.(tea.KeyMsg)
|
||||||
|
if !ok {
|
||||||
|
return m, m.listenForLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch keyMsg.String() {
|
||||||
|
case "up":
|
||||||
|
if m.selectedPrivesc > 0 {
|
||||||
|
m.selectedPrivesc--
|
||||||
|
}
|
||||||
|
case "down":
|
||||||
|
if m.selectedPrivesc < len(m.availablePrivesc)-1 {
|
||||||
|
m.selectedPrivesc++
|
||||||
|
}
|
||||||
|
case "enter":
|
||||||
|
chosen := m.availablePrivesc[m.selectedPrivesc]
|
||||||
|
if err := privesc.SetTool(chosen); err != nil {
|
||||||
|
m.err = fmt.Errorf("failed to select %s: %w", chosen.Name(), err)
|
||||||
|
m.state = StateError
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m.routeToAuthAfterPrivesc()
|
||||||
|
case "esc":
|
||||||
|
m.state = StateDependencyReview
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func privescToolDescription(t privesc.Tool) string {
|
||||||
|
switch t {
|
||||||
|
case privesc.ToolSudo:
|
||||||
|
return "classic sudo (supports password prompt in this installer)"
|
||||||
|
case privesc.ToolDoas:
|
||||||
|
return "OpenBSD-style doas (requires persist or nopass in /etc/doas.conf)"
|
||||||
|
case privesc.ToolRun0:
|
||||||
|
return "systemd run0 (authenticated via polkit)"
|
||||||
|
default:
|
||||||
|
return string(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// routeToAuthAfterPrivesc advances from the privesc-selection screen to the
|
||||||
|
// right auth flow. Sudo goes through the fingerprint/password path; doas and
|
||||||
|
// run0 skip password entry and proceed to install.
|
||||||
|
func (m Model) routeToAuthAfterPrivesc() (tea.Model, tea.Cmd) {
|
||||||
|
tool, err := privesc.Detect()
|
||||||
|
if err != nil {
|
||||||
|
m.err = err
|
||||||
|
m.state = StateError
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if tool == privesc.ToolSudo {
|
||||||
|
if checkFingerprintEnabled() {
|
||||||
|
m.state = StateAuthMethodChoice
|
||||||
|
m.selectedConfig = 0
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.state = StatePasswordPrompt
|
||||||
|
m.passwordInput.Focus()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.sudoPassword = ""
|
||||||
|
m.packageProgress = packageInstallProgressMsg{}
|
||||||
|
m.state = StateInstallingPackages
|
||||||
|
m.isLoading = true
|
||||||
|
return m, tea.Batch(m.spinner.Tick, m.installPackages())
|
||||||
|
}
|
||||||
|
|
||||||
|
// enterAuthPhase is called when dependency review (or the Gentoo screens)
|
||||||
|
// finish. It either routes directly to the sudo/fingerprint flow or shows
|
||||||
|
// the privesc-tool selection screen when multiple tools are available and
|
||||||
|
// no $DMS_PRIVESC override is set.
|
||||||
|
func (m Model) enterAuthPhase() (tea.Model, tea.Cmd) {
|
||||||
|
tools := privesc.AvailableTools()
|
||||||
|
_, envSet := privesc.EnvOverride()
|
||||||
|
|
||||||
|
if len(tools) == 0 {
|
||||||
|
m.err = fmt.Errorf("no supported privilege tool (sudo/doas/run0) found on PATH")
|
||||||
|
m.state = StateError
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if envSet || len(tools) == 1 {
|
||||||
|
return m.routeToAuthAfterPrivesc()
|
||||||
|
}
|
||||||
|
|
||||||
|
m.availablePrivesc = tools
|
||||||
|
m.selectedPrivesc = 0
|
||||||
|
m.state = StateSelectPrivesc
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
157
flake.nix
157
flake.nix
@@ -41,10 +41,11 @@
|
|||||||
nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] (
|
nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] (
|
||||||
system: fn system nixpkgs.legacyPackages.${system}
|
system: fn system nixpkgs.legacyPackages.${system}
|
||||||
);
|
);
|
||||||
buildDmsPkgs = pkgs: {
|
forEachLinuxSystem =
|
||||||
dms-shell = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
fn:
|
||||||
quickshell = quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
nixpkgs.lib.genAttrs [ "aarch64-linux" "x86_64-linux" ] (
|
||||||
};
|
system: fn system nixpkgs.legacyPackages.${system}
|
||||||
|
);
|
||||||
mkModuleWithDmsPkgs =
|
mkModuleWithDmsPkgs =
|
||||||
modulePath:
|
modulePath:
|
||||||
args@{ pkgs, ... }:
|
args@{ pkgs, ... }:
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
(import modulePath (args // { dmsPkgs = buildDmsPkgs pkgs; }))
|
(import modulePath (args // { dmsPkgs = buildDmsPkgs pkgs; }))
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
mkQmlImportPath =
|
mkQmlImportPath =
|
||||||
pkgs: qmlPkgs:
|
pkgs: qmlPkgs:
|
||||||
pkgs.lib.concatStringsSep ":" (map (o: "${o}/${pkgs.qt6.qtbase.qtQmlPrefix}") qmlPkgs);
|
pkgs.lib.concatStringsSep ":" (map (o: "${o}/${pkgs.qt6.qtbase.qtQmlPrefix}") qmlPkgs);
|
||||||
@@ -69,10 +71,11 @@
|
|||||||
qtimageformats
|
qtimageformats
|
||||||
kimageformats
|
kimageformats
|
||||||
];
|
];
|
||||||
in
|
|
||||||
{
|
# Allows downstream modules to provide their own 'pkgs' (with overlays)
|
||||||
packages = forEachSystem (
|
# instead of being forced to use the flake's locked nixpkgs.
|
||||||
system: pkgs:
|
mkDmsShell =
|
||||||
|
pkgs:
|
||||||
let
|
let
|
||||||
mkDate =
|
mkDate =
|
||||||
longDate:
|
longDate:
|
||||||
@@ -90,86 +93,96 @@
|
|||||||
in
|
in
|
||||||
"${cleanVersion}${dateSuffix}${revSuffix}";
|
"${cleanVersion}${dateSuffix}${revSuffix}";
|
||||||
in
|
in
|
||||||
{
|
pkgs.lib.makeOverridable (
|
||||||
dms-shell = pkgs.lib.makeOverridable (
|
{
|
||||||
|
extraQtPackages ? [ ],
|
||||||
|
}:
|
||||||
|
(pkgs.buildGoModule.override { go = goForPkgs pkgs; }) (
|
||||||
|
let
|
||||||
|
rootSrc = ./.;
|
||||||
|
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
extraQtPackages ? [ ],
|
inherit version;
|
||||||
}:
|
pname = "dms-shell";
|
||||||
(pkgs.buildGoModule.override { go = goForPkgs pkgs; }) (
|
src = ./core;
|
||||||
let
|
vendorHash = "sha256-cVUJXgzYMRSM0od1xzDVkMTdxHu3OIQX2bQ8AJbGQ1Q=";
|
||||||
rootSrc = ./.;
|
|
||||||
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit version;
|
|
||||||
pname = "dms-shell";
|
|
||||||
src = ./core;
|
|
||||||
vendorHash = "sha256-cVUJXgzYMRSM0od1xzDVkMTdxHu3OIQX2bQ8AJbGQ1Q=";
|
|
||||||
|
|
||||||
subPackages = [ "cmd/dms" ];
|
subPackages = [ "cmd/dms" ];
|
||||||
|
|
||||||
ldflags = [
|
ldflags = [
|
||||||
"-s"
|
"-s"
|
||||||
"-w"
|
"-w"
|
||||||
"-X 'main.Version=${version}'"
|
"-X 'main.Version=${version}'"
|
||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
installShellFiles
|
installShellFiles
|
||||||
makeWrapper
|
makeWrapper
|
||||||
];
|
];
|
||||||
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
mkdir -p $out/share/quickshell/dms
|
mkdir -p $out/share/quickshell/dms
|
||||||
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
||||||
|
|
||||||
chmod u+w $out/share/quickshell/dms/VERSION
|
chmod u+w $out/share/quickshell/dms/VERSION
|
||||||
echo "${version}" > $out/share/quickshell/dms/VERSION
|
echo "${version}" > $out/share/quickshell/dms/VERSION
|
||||||
|
|
||||||
# Install desktop file and icon
|
# Install desktop file and icon
|
||||||
install -D ${rootSrc}/assets/dms-open.desktop \
|
install -D ${rootSrc}/assets/dms-open.desktop \
|
||||||
$out/share/applications/dms-open.desktop
|
$out/share/applications/dms-open.desktop
|
||||||
install -D ${rootSrc}/core/assets/danklogo.svg \
|
install -D ${rootSrc}/core/assets/danklogo.svg \
|
||||||
$out/share/hicolor/scalable/apps/danklogo.svg
|
$out/share/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
wrapProgram $out/bin/dms \
|
wrapProgram $out/bin/dms \
|
||||||
--add-flags "-c $out/share/quickshell/dms" \
|
--add-flags "-c $out/share/quickshell/dms" \
|
||||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
|
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
|
||||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
|
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
|
||||||
|
|
||||||
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
||||||
$out/lib/systemd/user/dms.service
|
$out/lib/systemd/user/dms.service
|
||||||
|
|
||||||
substituteInPlace $out/lib/systemd/user/dms.service \
|
substituteInPlace $out/lib/systemd/user/dms.service \
|
||||||
--replace-fail /usr/bin/dms $out/bin/dms \
|
--replace-fail /usr/bin/dms $out/bin/dms \
|
||||||
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
||||||
|
|
||||||
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
||||||
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
||||||
|
|
||||||
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
||||||
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
||||||
|
|
||||||
installShellCompletion --cmd dms \
|
substituteInPlace $out/share/quickshell/dms/assets/pam/u2f \
|
||||||
--bash <($out/bin/dms completion bash) \
|
--replace-fail pam_u2f.so ${pkgs.pam_u2f}/lib/security/pam_u2f.so
|
||||||
--fish <($out/bin/dms completion fish) \
|
|
||||||
--zsh <($out/bin/dms completion zsh)
|
|
||||||
'';
|
|
||||||
|
|
||||||
meta = {
|
installShellCompletion --cmd dms \
|
||||||
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
--bash <($out/bin/dms completion bash) \
|
||||||
homepage = "https://danklinux.com";
|
--fish <($out/bin/dms completion fish) \
|
||||||
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
--zsh <($out/bin/dms completion zsh)
|
||||||
license = pkgs.lib.licenses.mit;
|
'';
|
||||||
mainProgram = "dms";
|
|
||||||
platforms = pkgs.lib.platforms.linux;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) { };
|
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
||||||
|
homepage = "https://danklinux.com";
|
||||||
|
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
||||||
|
license = pkgs.lib.licenses.mit;
|
||||||
|
mainProgram = "dms";
|
||||||
|
platforms = pkgs.lib.platforms.linux;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) { };
|
||||||
|
|
||||||
|
buildDmsPkgs = pkgs: {
|
||||||
|
dms-shell = mkDmsShell pkgs;
|
||||||
|
quickshell = quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = forEachSystem (
|
||||||
|
system: pkgs: {
|
||||||
|
dms-shell = mkDmsShell pkgs;
|
||||||
quickshell = quickshell.packages.${system}.default;
|
quickshell = quickshell.packages.${system}.default;
|
||||||
|
|
||||||
default = self.packages.${system}.dms-shell;
|
default = self.packages.${system}.dms-shell;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -115,4 +115,8 @@ Singleton {
|
|||||||
return translations[context][term];
|
return translations[context][term];
|
||||||
return term;
|
return term;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function locale() {
|
||||||
|
return Qt.locale();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -314,12 +314,6 @@ Singleton {
|
|||||||
property bool greeterEnableFprint: false
|
property bool greeterEnableFprint: false
|
||||||
property bool greeterEnableU2f: false
|
property bool greeterEnableU2f: false
|
||||||
property string greeterWallpaperPath: ""
|
property string greeterWallpaperPath: ""
|
||||||
property bool greeterUse24HourClock: true
|
|
||||||
property bool greeterShowSeconds: false
|
|
||||||
property bool greeterPadHours12Hour: false
|
|
||||||
property string greeterLockDateFormat: ""
|
|
||||||
property string greeterFontFamily: ""
|
|
||||||
property string greeterWallpaperFillMode: ""
|
|
||||||
property int mediaSize: 1
|
property int mediaSize: 1
|
||||||
|
|
||||||
property string appLauncherViewMode: "list"
|
property string appLauncherViewMode: "list"
|
||||||
@@ -448,6 +442,11 @@ Singleton {
|
|||||||
property bool syncModeWithPortal: true
|
property bool syncModeWithPortal: true
|
||||||
property bool terminalsAlwaysDark: false
|
property bool terminalsAlwaysDark: false
|
||||||
|
|
||||||
|
property string muxType: "tmux"
|
||||||
|
property bool muxUseCustomCommand: false
|
||||||
|
property string muxCustomCommand: ""
|
||||||
|
property string muxSessionFilter: ""
|
||||||
|
|
||||||
property bool runDmsMatugenTemplates: true
|
property bool runDmsMatugenTemplates: true
|
||||||
property bool matugenTemplateGtk: true
|
property bool matugenTemplateGtk: true
|
||||||
property bool matugenTemplateNiri: true
|
property bool matugenTemplateNiri: true
|
||||||
|
|||||||
@@ -168,12 +168,6 @@ var SPEC = {
|
|||||||
greeterEnableFprint: { def: false },
|
greeterEnableFprint: { def: false },
|
||||||
greeterEnableU2f: { def: false },
|
greeterEnableU2f: { def: false },
|
||||||
greeterWallpaperPath: { def: "" },
|
greeterWallpaperPath: { def: "" },
|
||||||
greeterUse24HourClock: { def: true },
|
|
||||||
greeterShowSeconds: { def: false },
|
|
||||||
greeterPadHours12Hour: { def: false },
|
|
||||||
greeterLockDateFormat: { def: "" },
|
|
||||||
greeterFontFamily: { def: "" },
|
|
||||||
greeterWallpaperFillMode: { def: "" },
|
|
||||||
mediaSize: { def: 1 },
|
mediaSize: { def: 1 },
|
||||||
|
|
||||||
appLauncherViewMode: { def: "list" },
|
appLauncherViewMode: { def: "list" },
|
||||||
@@ -266,6 +260,11 @@ var SPEC = {
|
|||||||
syncModeWithPortal: { def: true },
|
syncModeWithPortal: { def: true },
|
||||||
terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" },
|
terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" },
|
||||||
|
|
||||||
|
muxType: { def: "tmux" },
|
||||||
|
muxUseCustomCommand: { def: false },
|
||||||
|
muxCustomCommand: { def: "" },
|
||||||
|
muxSessionFilter: { def: "" },
|
||||||
|
|
||||||
runDmsMatugenTemplates: { def: true },
|
runDmsMatugenTemplates: { def: true },
|
||||||
matugenTemplateGtk: { def: true },
|
matugenTemplateGtk: { def: true },
|
||||||
matugenTemplateNiri: { def: true },
|
matugenTemplateNiri: { def: true },
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import qs.Modals.Clipboard
|
|||||||
import qs.Modals.Greeter
|
import qs.Modals.Greeter
|
||||||
import qs.Modals.Settings
|
import qs.Modals.Settings
|
||||||
import qs.Modals.DankLauncherV2
|
import qs.Modals.DankLauncherV2
|
||||||
|
import qs.Modals
|
||||||
import qs.Modules
|
import qs.Modules
|
||||||
import qs.Modules.AppDrawer
|
import qs.Modules.AppDrawer
|
||||||
import qs.Modules.DankDash
|
import qs.Modules.DankDash
|
||||||
@@ -638,6 +639,10 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MuxModal {
|
||||||
|
id: muxModal
|
||||||
|
}
|
||||||
|
|
||||||
ClipboardHistoryModal {
|
ClipboardHistoryModal {
|
||||||
id: clipboardHistoryModalPopup
|
id: clipboardHistoryModalPopup
|
||||||
|
|
||||||
@@ -1018,12 +1023,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: powerProfileWatcherLoader
|
|
||||||
active: SettingsData.osdPowerProfileEnabled
|
|
||||||
source: "Services/PowerProfileWatcher.qml"
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
LazyLoader {
|
||||||
id: hyprlandOverviewLoader
|
id: hyprlandOverviewLoader
|
||||||
active: CompositorService.isHyprland
|
active: CompositorService.isHyprland
|
||||||
|
|||||||
312
quickshell/Modals/Common/InputModal.qml
Normal file
312
quickshell/Modals/Common/InputModal.qml
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
layerNamespace: "dms:input-modal"
|
||||||
|
keepPopoutsOpen: true
|
||||||
|
|
||||||
|
property string inputTitle: ""
|
||||||
|
property string inputMessage: ""
|
||||||
|
property string inputPlaceholder: ""
|
||||||
|
property string inputText: ""
|
||||||
|
property string confirmButtonText: "Confirm"
|
||||||
|
property string cancelButtonText: "Cancel"
|
||||||
|
property color confirmButtonColor: Theme.primary
|
||||||
|
property var onConfirm: function (text) {}
|
||||||
|
property var onCancel: function () {}
|
||||||
|
property int selectedButton: -1
|
||||||
|
property bool keyboardNavigation: false
|
||||||
|
|
||||||
|
function show(title, message, onConfirmCallback, onCancelCallback) {
|
||||||
|
inputTitle = title || "";
|
||||||
|
inputMessage = message || "";
|
||||||
|
inputPlaceholder = "";
|
||||||
|
inputText = "";
|
||||||
|
confirmButtonText = "Confirm";
|
||||||
|
cancelButtonText = "Cancel";
|
||||||
|
confirmButtonColor = Theme.primary;
|
||||||
|
onConfirm = onConfirmCallback || ((text) => {});
|
||||||
|
onCancel = onCancelCallback || (() => {});
|
||||||
|
selectedButton = -1;
|
||||||
|
keyboardNavigation = false;
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWithOptions(options) {
|
||||||
|
inputTitle = options.title || "";
|
||||||
|
inputMessage = options.message || "";
|
||||||
|
inputPlaceholder = options.placeholder || "";
|
||||||
|
inputText = options.initialText || "";
|
||||||
|
confirmButtonText = options.confirmText || "Confirm";
|
||||||
|
cancelButtonText = options.cancelText || "Cancel";
|
||||||
|
confirmButtonColor = options.confirmColor || Theme.primary;
|
||||||
|
onConfirm = options.onConfirm || ((text) => {});
|
||||||
|
onCancel = options.onCancel || (() => {});
|
||||||
|
selectedButton = -1;
|
||||||
|
keyboardNavigation = false;
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmAndClose() {
|
||||||
|
const text = inputText;
|
||||||
|
close();
|
||||||
|
if (onConfirm) {
|
||||||
|
onConfirm(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelAndClose() {
|
||||||
|
close();
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectButton() {
|
||||||
|
if (selectedButton === 0) {
|
||||||
|
cancelAndClose();
|
||||||
|
} else {
|
||||||
|
confirmAndClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldBeVisible: false
|
||||||
|
allowStacking: true
|
||||||
|
modalWidth: 350
|
||||||
|
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200
|
||||||
|
enableShadow: true
|
||||||
|
shouldHaveFocus: true
|
||||||
|
onBackgroundClicked: cancelAndClose()
|
||||||
|
onOpened: {
|
||||||
|
Qt.callLater(function () {
|
||||||
|
if (contentLoader.item && contentLoader.item.textInputRef) {
|
||||||
|
contentLoader.item.textInputRef.forceActiveFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
FocusScope {
|
||||||
|
anchors.fill: parent
|
||||||
|
implicitHeight: mainColumn.implicitHeight
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
property alias textInputRef: textInput
|
||||||
|
|
||||||
|
Keys.onPressed: function (event) {
|
||||||
|
const textFieldFocused = textInput.activeFocus;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Escape:
|
||||||
|
root.cancelAndClose();
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_Tab:
|
||||||
|
if (textFieldFocused) {
|
||||||
|
root.keyboardNavigation = true;
|
||||||
|
root.selectedButton = 0;
|
||||||
|
textInput.focus = false;
|
||||||
|
} else {
|
||||||
|
root.keyboardNavigation = true;
|
||||||
|
if (root.selectedButton === -1) {
|
||||||
|
root.selectedButton = 0;
|
||||||
|
} else if (root.selectedButton === 0) {
|
||||||
|
root.selectedButton = 1;
|
||||||
|
} else {
|
||||||
|
root.selectedButton = -1;
|
||||||
|
textInput.forceActiveFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_Left:
|
||||||
|
if (!textFieldFocused) {
|
||||||
|
root.keyboardNavigation = true;
|
||||||
|
root.selectedButton = 0;
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_Right:
|
||||||
|
if (!textFieldFocused) {
|
||||||
|
root.keyboardNavigation = true;
|
||||||
|
root.selectedButton = 1;
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
if (root.selectedButton !== -1) {
|
||||||
|
root.selectButton();
|
||||||
|
} else {
|
||||||
|
root.confirmAndClose();
|
||||||
|
}
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingL
|
||||||
|
anchors.rightMargin: Theme.spacingL
|
||||||
|
anchors.topMargin: Theme.spacingL
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.inputTitle
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: Theme.spacingL
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.inputMessage
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
visible: root.inputMessage !== ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: root.inputMessage !== "" ? Theme.spacingL : 0
|
||||||
|
visible: root.inputMessage !== ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceVariantAlpha
|
||||||
|
border.color: textInput.activeFocus ? Theme.primary : "transparent"
|
||||||
|
border.width: textInput.activeFocus ? 1 : 0
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: textInput
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
selectionColor: Theme.primary
|
||||||
|
selectedTextColor: Theme.primaryText
|
||||||
|
clip: true
|
||||||
|
text: root.inputText
|
||||||
|
onTextChanged: root.inputText = text
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.fill: parent
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
||||||
|
text: root.inputPlaceholder
|
||||||
|
visible: textInput.text === "" && !textInput.activeFocus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: Theme.spacingL * 1.5
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 120
|
||||||
|
height: 40
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (root.keyboardNavigation && root.selectedButton === 0) {
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||||
|
} else if (cancelButton.containsMouse) {
|
||||||
|
return Theme.surfacePressed;
|
||||||
|
} else {
|
||||||
|
return Theme.surfaceVariantAlpha;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: (root.keyboardNavigation && root.selectedButton === 0) ? Theme.primary : "transparent"
|
||||||
|
border.width: (root.keyboardNavigation && root.selectedButton === 0) ? 1 : 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.cancelButtonText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: cancelButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.cancelAndClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 120
|
||||||
|
height: 40
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
const baseColor = root.confirmButtonColor;
|
||||||
|
if (root.keyboardNavigation && root.selectedButton === 1) {
|
||||||
|
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1);
|
||||||
|
} else if (confirmButton.containsMouse) {
|
||||||
|
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9);
|
||||||
|
} else {
|
||||||
|
return baseColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
border.color: (root.keyboardNavigation && root.selectedButton === 1) ? "white" : "transparent"
|
||||||
|
border.width: (root.keyboardNavigation && root.selectedButton === 1) ? 1 : 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.confirmButtonText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.primaryText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: confirmButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.confirmAndClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: Theme.spacingL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,7 +81,7 @@ DankModal {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
Layout.alignment: Qt.AlignLeft
|
Layout.alignment: Qt.AlignLeft
|
||||||
text: KeybindsService.cheatsheet.title || "Keybinds"
|
text: KeybindsService.cheatsheet.title || i18n("Keybinds")
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
@@ -309,10 +309,12 @@ DankModal {
|
|||||||
id: keyText
|
id: keyText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: Theme.secondary
|
color: Theme.secondary
|
||||||
text: modelData.key || ""
|
text: (modelData.key || "").replace(/\+/g, " + ")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
isMonospace: true
|
isMonospace: true
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: Math.min(implicitWidth, 148)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +327,7 @@ DankModal {
|
|||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
opacity: 0.9
|
opacity: 0.9
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
621
quickshell/Modals/MuxModal.qml
Normal file
621
quickshell/Modals/MuxModal.qml
Normal file
@@ -0,0 +1,621 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: muxModal
|
||||||
|
|
||||||
|
layerNamespace: "dms:mux"
|
||||||
|
|
||||||
|
property int selectedIndex: -1
|
||||||
|
property string searchText: ""
|
||||||
|
property var filteredSessions: []
|
||||||
|
|
||||||
|
function updateFilteredSessions() {
|
||||||
|
var filtered = []
|
||||||
|
var lowerSearch = searchText.trim().toLowerCase()
|
||||||
|
for (var i = 0; i < MuxService.sessions.length; i++) {
|
||||||
|
var session = MuxService.sessions[i]
|
||||||
|
if (lowerSearch.length > 0 && !session.name.toLowerCase().includes(lowerSearch))
|
||||||
|
continue
|
||||||
|
filtered.push(session)
|
||||||
|
}
|
||||||
|
filteredSessions = filtered
|
||||||
|
|
||||||
|
if (selectedIndex >= filteredSessions.length) {
|
||||||
|
selectedIndex = Math.max(0, filteredSessions.length - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchTextChanged: updateFilteredSessions()
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: MuxService
|
||||||
|
function onSessionsChanged() {
|
||||||
|
updateFilteredSessions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
id: grab
|
||||||
|
windows: [muxModal.contentWindow]
|
||||||
|
active: CompositorService.isHyprland && muxModal.shouldHaveFocus
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
hide()
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
open()
|
||||||
|
selectedIndex = -1
|
||||||
|
searchText = ""
|
||||||
|
MuxService.refreshSessions()
|
||||||
|
shouldHaveFocus = true
|
||||||
|
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (muxPanel && muxPanel.searchField) {
|
||||||
|
muxPanel.searchField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
close()
|
||||||
|
selectedIndex = -1
|
||||||
|
searchText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachToSession(name) {
|
||||||
|
MuxService.attachToSession(name)
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameSession(name) {
|
||||||
|
inputModal.showWithOptions({
|
||||||
|
title: I18n.tr("Rename Session"),
|
||||||
|
message: I18n.tr("Enter a new name for session \"%1\"").arg(name),
|
||||||
|
initialText: name,
|
||||||
|
onConfirm: function (newName) {
|
||||||
|
MuxService.renameSession(name, newName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function killSession(name) {
|
||||||
|
confirmModal.showWithOptions({
|
||||||
|
title: I18n.tr("Kill Session"),
|
||||||
|
message: I18n.tr("Are you sure you want to kill session \"%1\"?").arg(name),
|
||||||
|
confirmText: I18n.tr("Kill"),
|
||||||
|
confirmColor: Theme.primary,
|
||||||
|
onConfirm: function () {
|
||||||
|
MuxService.killSession(name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewSession() {
|
||||||
|
inputModal.showWithOptions({
|
||||||
|
title: I18n.tr("New Session"),
|
||||||
|
message: I18n.tr("Please write a name for your new %1 session").arg(MuxService.displayName),
|
||||||
|
onConfirm: function (name) {
|
||||||
|
MuxService.createSession(name)
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNext() {
|
||||||
|
selectedIndex = Math.min(selectedIndex + 1, filteredSessions.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPrevious() {
|
||||||
|
selectedIndex = Math.max(selectedIndex - 1, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateSelected() {
|
||||||
|
if (selectedIndex === -1) {
|
||||||
|
createNewSession()
|
||||||
|
} else if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
|
||||||
|
attachToSession(filteredSessions[selectedIndex].name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
modalWidth: 600
|
||||||
|
modalHeight: 600
|
||||||
|
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
borderColor: Theme.outlineMedium
|
||||||
|
borderWidth: 1
|
||||||
|
enableShadow: true
|
||||||
|
keepContentLoaded: true
|
||||||
|
|
||||||
|
onBackgroundClicked: hide()
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 3000
|
||||||
|
running: muxModal.shouldBeVisible
|
||||||
|
repeat: true
|
||||||
|
onTriggered: MuxService.refreshSessions()
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
muxModal.show()
|
||||||
|
return "MUX_OPEN_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
muxModal.hide()
|
||||||
|
return "MUX_CLOSE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
muxModal.toggle()
|
||||||
|
return "MUX_TOGGLE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "mux"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backwards compatibility
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
muxModal.show()
|
||||||
|
return "TMUX_OPEN_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
muxModal.hide()
|
||||||
|
return "TMUX_CLOSE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
muxModal.toggle()
|
||||||
|
return "TMUX_TOGGLE_SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "tmux"
|
||||||
|
}
|
||||||
|
|
||||||
|
InputModal {
|
||||||
|
id: inputModal
|
||||||
|
onShouldBeVisibleChanged: {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
muxModal.shouldHaveFocus = false;
|
||||||
|
muxModal.contentWindow.visible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (muxModal.shouldBeVisible) {
|
||||||
|
muxModal.contentWindow.visible = true;
|
||||||
|
}
|
||||||
|
Qt.callLater(function () {
|
||||||
|
if (!muxModal.shouldBeVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
muxModal.shouldHaveFocus = true;
|
||||||
|
muxModal.modalFocusScope.forceActiveFocus();
|
||||||
|
if (muxPanel.searchField) {
|
||||||
|
muxPanel.searchField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfirmModal {
|
||||||
|
id: confirmModal
|
||||||
|
onShouldBeVisibleChanged: {
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
muxModal.shouldHaveFocus = false;
|
||||||
|
muxModal.contentWindow.visible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (muxModal.shouldBeVisible) {
|
||||||
|
muxModal.contentWindow.visible = true;
|
||||||
|
}
|
||||||
|
Qt.callLater(function () {
|
||||||
|
if (!muxModal.shouldBeVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
muxModal.shouldHaveFocus = true;
|
||||||
|
muxModal.modalFocusScope.forceActiveFocus();
|
||||||
|
if (muxPanel.searchField) {
|
||||||
|
muxPanel.searchField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
directContent: Item {
|
||||||
|
id: muxPanel
|
||||||
|
|
||||||
|
clip: false
|
||||||
|
|
||||||
|
property alias searchField: searchField
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if ((event.key === Qt.Key_J && (event.modifiers & Qt.ControlModifier)) ||
|
||||||
|
(event.key === Qt.Key_Down)) {
|
||||||
|
selectNext()
|
||||||
|
event.accepted = true
|
||||||
|
} else if ((event.key === Qt.Key_K && (event.modifiers & Qt.ControlModifier)) ||
|
||||||
|
(event.key === Qt.Key_Up)) {
|
||||||
|
selectPrevious()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_N && (event.modifiers & Qt.ControlModifier)) {
|
||||||
|
createNewSession()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_R && (event.modifiers & Qt.ControlModifier)) {
|
||||||
|
if (MuxService.supportsRename && selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
|
||||||
|
renameSession(filteredSessions[selectedIndex].name)
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_D && (event.modifiers & Qt.ControlModifier)) {
|
||||||
|
if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
|
||||||
|
killSession(filteredSessions[selectedIndex].name)
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Escape) {
|
||||||
|
hide()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
activateSelected()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - Theme.spacingM * 2
|
||||||
|
height: parent.height - Theme.spacingM * 2
|
||||||
|
x: Theme.spacingM
|
||||||
|
y: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
// Header
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: I18n.tr("%1 Sessions").arg(MuxService.displayName)
|
||||||
|
font.pixelSize: Theme.fontSizeLarge + 4
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: I18n.tr("%1 active, %2 filtered").arg(MuxService.sessions.length).arg(muxModal.filteredSessions.length)
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search field
|
||||||
|
DankTextField {
|
||||||
|
id: searchField
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 48
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
backgroundColor: Theme.surfaceContainerHigh
|
||||||
|
normalBorderColor: Theme.outlineMedium
|
||||||
|
focusedBorderColor: Theme.primary
|
||||||
|
leftIconName: "search"
|
||||||
|
leftIconSize: Theme.iconSize
|
||||||
|
leftIconColor: Theme.surfaceVariantText
|
||||||
|
leftIconFocusedColor: Theme.primary
|
||||||
|
showClearButton: true
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
placeholderText: I18n.tr("Search sessions...")
|
||||||
|
keyForwardTargets: [muxPanel]
|
||||||
|
|
||||||
|
onTextEdited: {
|
||||||
|
muxModal.searchText = text
|
||||||
|
muxModal.selectedIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Session Button
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 56
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: muxModal.selectedIndex === -1 ? Theme.primaryContainer :
|
||||||
|
(newMouse.containsMouse ? Theme.surfaceContainerHigh : Theme.surfaceContainer)
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 40
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
radius: 20
|
||||||
|
color: Theme.primaryContainer
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "add"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("New Session")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Create a new %1 session (n)").arg(MuxService.displayName)
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: newMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: muxModal.createNewSession()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sessions List
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 88 - 48 - shortcutsBar.height - Theme.spacingS * 3
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ScriptModel {
|
||||||
|
values: muxModal.filteredSessions
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 64
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: muxModal.selectedIndex === index ? Theme.primaryContainer :
|
||||||
|
(sessionMouse.containsMouse ? Theme.surfaceContainerHigh : "transparent")
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: sessionMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: muxModal.attachToSession(modelData.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
// Avatar
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 40
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
radius: 20
|
||||||
|
color: modelData.attached ? Theme.primaryContainer : Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData.name.charAt(0).toUpperCase()
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: modelData.attached ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info
|
||||||
|
Column {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.name
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
var parts = []
|
||||||
|
if (modelData.windows !== "N/A")
|
||||||
|
parts.push(I18n.tr("%1 windows").arg(modelData.windows))
|
||||||
|
parts.push(modelData.attached ? I18n.tr("attached") : I18n.tr("detached"))
|
||||||
|
return parts.join(" \u2022 ")
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename button (tmux only)
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 36
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
radius: 18
|
||||||
|
visible: MuxService.supportsRename
|
||||||
|
color: renameMouse.containsMouse ? Theme.surfaceContainerHighest : "transparent"
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "edit"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: renameMouse.containsMouse ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: renameMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: muxModal.renameSession(modelData.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 36
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
radius: 18
|
||||||
|
color: deleteMouse.containsMouse ? Theme.errorContainer : "transparent"
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "delete"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: deleteMouse.containsMouse ? Theme.error : Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deleteMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
muxModal.killSession(modelData.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty state
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: muxModal.filteredSessions.length === 0 ? 200 : 0
|
||||||
|
visible: muxModal.filteredSessions.length === 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: muxModal.searchText.length > 0 ? "search_off" : "terminal"
|
||||||
|
size: 48
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: muxModal.searchText.length > 0 ? I18n.tr("No sessions found") : I18n.tr("No active %1 sessions").arg(MuxService.displayName)
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: muxModal.searchText.length > 0 ? I18n.tr("Try a different search") : I18n.tr("Press 'n' or click 'New Session' to create one")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shortcuts bar
|
||||||
|
Row {
|
||||||
|
id: shortcutsBar
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
bottomPadding: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: {
|
||||||
|
var shortcuts = [
|
||||||
|
{ key: "↑↓", label: I18n.tr("Navigate") },
|
||||||
|
{ key: "↵", label: I18n.tr("Attach") },
|
||||||
|
{ key: "^N", label: I18n.tr("New") },
|
||||||
|
{ key: "^D", label: I18n.tr("Kill") },
|
||||||
|
{ key: "Esc", label: I18n.tr("Close") }
|
||||||
|
]
|
||||||
|
if (MuxService.supportsRename)
|
||||||
|
shortcuts.splice(3, 0, { key: "^R", label: I18n.tr("Rename") })
|
||||||
|
return shortcuts
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Row {
|
||||||
|
required property var modelData
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: keyText.width + Theme.spacingS
|
||||||
|
height: keyText.height + 4
|
||||||
|
radius: 4
|
||||||
|
color: Theme.surfaceContainerHighest
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: keyText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData.key
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.label
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -473,5 +473,20 @@ FocusScope {
|
|||||||
Qt.callLater(() => item.forceActiveFocus());
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: muxLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 32
|
||||||
|
visible: active
|
||||||
|
focus: active
|
||||||
|
|
||||||
|
sourceComponent: MuxTab {}
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active && item)
|
||||||
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ Rectangle {
|
|||||||
{
|
{
|
||||||
"id": "running_apps",
|
"id": "running_apps",
|
||||||
"text": I18n.tr("Running Apps"),
|
"text": I18n.tr("Running Apps"),
|
||||||
"icon": "apps",
|
"icon": "app_registration",
|
||||||
"tabIndex": 19,
|
"tabIndex": 19,
|
||||||
"hyprlandNiriOnly": true
|
"hyprlandNiriOnly": true
|
||||||
},
|
},
|
||||||
@@ -237,7 +237,7 @@ Rectangle {
|
|||||||
{
|
{
|
||||||
"id": "system",
|
"id": "system",
|
||||||
"text": I18n.tr("System"),
|
"text": I18n.tr("System"),
|
||||||
"icon": "computer",
|
"icon": "memory",
|
||||||
"collapsedByDefault": true,
|
"collapsedByDefault": true,
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
@@ -260,6 +260,12 @@ Rectangle {
|
|||||||
"tabIndex": 8,
|
"tabIndex": 8,
|
||||||
"cupsOnly": true
|
"cupsOnly": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "multiplexers",
|
||||||
|
"text": I18n.tr("Multiplexers"),
|
||||||
|
"icon": "terminal",
|
||||||
|
"tabIndex": 32
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "window_rules",
|
"id": "window_rules",
|
||||||
"text": I18n.tr("Window Rules"),
|
"text": I18n.tr("Window Rules"),
|
||||||
|
|||||||
@@ -10,6 +10,36 @@ BasePill {
|
|||||||
|
|
||||||
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
||||||
readonly property bool playerAvailable: activePlayer !== null
|
readonly property bool playerAvailable: activePlayer !== null
|
||||||
|
readonly property bool _hoverPreview: MprisController.isFirefoxYoutubeHoverPreview(activePlayer)
|
||||||
|
readonly property bool _isPlaying: !!activePlayer && activePlayer.playbackState === 1 && !_hoverPreview
|
||||||
|
|
||||||
|
property string _stableTitle: ""
|
||||||
|
property string _stableArtist: ""
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.activePlayer
|
||||||
|
function onTrackTitleChanged() {
|
||||||
|
root._syncMeta();
|
||||||
|
}
|
||||||
|
function onTrackArtistChanged() {
|
||||||
|
root._syncMeta();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onActivePlayerChanged: _syncMeta()
|
||||||
|
|
||||||
|
function _syncMeta() {
|
||||||
|
if (!activePlayer) {
|
||||||
|
_stableTitle = "";
|
||||||
|
_stableArtist = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (MprisController.isFirefoxYoutubeHoverPreview(activePlayer))
|
||||||
|
return;
|
||||||
|
_stableTitle = activePlayer.trackTitle || "";
|
||||||
|
_stableArtist = activePlayer.trackArtist || "";
|
||||||
|
}
|
||||||
|
|
||||||
readonly property bool __isChromeBrowser: {
|
readonly property bool __isChromeBrowser: {
|
||||||
if (!activePlayer?.identity)
|
if (!activePlayer?.identity)
|
||||||
return false;
|
return false;
|
||||||
@@ -191,15 +221,15 @@ BasePill {
|
|||||||
height: 24
|
height: 24
|
||||||
radius: 12
|
radius: 12
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover
|
color: root._isPlaying ? Theme.primary : Theme.primaryHover
|
||||||
visible: root.playerAvailable
|
visible: root.playerAvailable
|
||||||
opacity: activePlayer ? 1 : 0.3
|
opacity: activePlayer ? 1 : 0.3
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow"
|
name: root._isPlaying ? "pause" : "play_arrow"
|
||||||
size: 14
|
size: 14
|
||||||
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary
|
color: root._isPlaying ? Theme.background : Theme.primary
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -258,12 +288,10 @@ BasePill {
|
|||||||
readonly property bool isWebMedia: lowerIdentity.includes("firefox") || lowerIdentity.includes("chrome") || lowerIdentity.includes("chromium") || lowerIdentity.includes("edge") || lowerIdentity.includes("safari")
|
readonly property bool isWebMedia: lowerIdentity.includes("firefox") || lowerIdentity.includes("chrome") || lowerIdentity.includes("chromium") || lowerIdentity.includes("edge") || lowerIdentity.includes("safari")
|
||||||
|
|
||||||
property string displayText: {
|
property string displayText: {
|
||||||
if (!activePlayer || !activePlayer.trackTitle) {
|
if (!activePlayer || !root._stableTitle)
|
||||||
return "";
|
return "";
|
||||||
}
|
const title = isWebMedia ? root._stableTitle : (root._stableTitle || "Unknown Track");
|
||||||
|
const subtitle = isWebMedia ? (root._stableArtist || cachedIdentity) : (root._stableArtist || "");
|
||||||
const title = isWebMedia ? activePlayer.trackTitle : (activePlayer.trackTitle || "Unknown Track");
|
|
||||||
const subtitle = isWebMedia ? (activePlayer.trackArtist || cachedIdentity) : (activePlayer.trackArtist || "");
|
|
||||||
return subtitle.length > 0 ? title + " • " + subtitle : title;
|
return subtitle.length > 0 ? title + " • " + subtitle : title;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,15 +409,15 @@ BasePill {
|
|||||||
height: 24
|
height: 24
|
||||||
radius: 12
|
radius: 12
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover
|
color: root._isPlaying ? Theme.primary : Theme.primaryHover
|
||||||
visible: root.playerAvailable
|
visible: root.playerAvailable
|
||||||
opacity: activePlayer ? 1 : 0.3
|
opacity: activePlayer ? 1 : 0.3
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow"
|
name: root._isPlaying ? "pause" : "play_arrow"
|
||||||
size: 14
|
size: 14
|
||||||
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary
|
color: root._isPlaying ? Theme.background : Theme.primary
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ BasePill {
|
|||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: {
|
color: {
|
||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
|
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45);
|
||||||
}
|
}
|
||||||
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
|
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
|
||||||
}
|
}
|
||||||
@@ -526,7 +526,7 @@ BasePill {
|
|||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: {
|
color: {
|
||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
|
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45);
|
||||||
}
|
}
|
||||||
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
|
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,11 @@ BasePill {
|
|||||||
enableCursor: false
|
enableCursor: false
|
||||||
|
|
||||||
property var parentWindow: null
|
property var parentWindow: null
|
||||||
|
property var widgetData: null
|
||||||
|
property string section: "right"
|
||||||
property bool isAtBottom: false
|
property bool isAtBottom: false
|
||||||
property bool isAutoHideBar: false
|
property bool isAutoHideBar: false
|
||||||
|
property bool useOverflowPopup: !widgetData?.trayUseInlineExpansion
|
||||||
readonly property var hiddenTrayIds: {
|
readonly property var hiddenTrayIds: {
|
||||||
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || "";
|
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || "";
|
||||||
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : [];
|
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : [];
|
||||||
@@ -41,6 +44,54 @@ BasePill {
|
|||||||
return `${id}::${tooltipTitle}`;
|
return `${id}::${tooltipTitle}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trayIconSourceFor(trayItem) {
|
||||||
|
let icon = trayItem && trayItem.icon;
|
||||||
|
if (typeof icon === 'string' || icon instanceof String) {
|
||||||
|
if (icon === "")
|
||||||
|
return "";
|
||||||
|
if (icon.includes("?path=")) {
|
||||||
|
const split = icon.split("?path=");
|
||||||
|
if (split.length !== 2)
|
||||||
|
return icon;
|
||||||
|
const name = split[0];
|
||||||
|
const path = split[1];
|
||||||
|
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
||||||
|
if (fileName.startsWith("dropboxstatus")) {
|
||||||
|
fileName = `hicolor/16x16/status/${fileName}`;
|
||||||
|
}
|
||||||
|
return `file://${path}/${fileName}`;
|
||||||
|
}
|
||||||
|
if (icon.startsWith("/") && !icon.startsWith("file://"))
|
||||||
|
return `file://${icon}`;
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateInlineTrayItem(trayItem, anchorItem) {
|
||||||
|
if (!trayItem)
|
||||||
|
return;
|
||||||
|
if (!trayItem.onlyMenu) {
|
||||||
|
trayItem.activate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!trayItem.hasMenu)
|
||||||
|
return;
|
||||||
|
root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openInlineTrayContextMenu(trayItem, areaItem, mouse, anchorItem) {
|
||||||
|
if (!trayItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!trayItem.hasMenu) {
|
||||||
|
const gp = areaItem.mapToGlobal(mouse.x, mouse.y);
|
||||||
|
root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||||
|
}
|
||||||
|
|
||||||
// ! TODO - replace with either native dbus client (like plugins use) or just a DMS cli or something
|
// ! TODO - replace with either native dbus client (like plugins use) or just a DMS cli or something
|
||||||
function callContextMenuFallback(trayItemId, globalX, globalY) {
|
function callContextMenuFallback(trayItemId, globalX, globalY) {
|
||||||
const script = ['ITEMS=$(dbus-send --session --print-reply --dest=org.kde.StatusNotifierWatcher /StatusNotifierWatcher org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierWatcher string:RegisteredStatusNotifierItems 2>/dev/null)', 'while IFS= read -r line; do', ' line="${line#*\\\"}"', ' line="${line%\\\"*}"', ' [ -z "$line" ] && continue', ' BUS="${line%%/*}"', ' OBJ="/${line#*/}"', ' ID=$(dbus-send --session --print-reply --dest="$BUS" "$OBJ" org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierItem string:Id 2>/dev/null | grep -oP "(?<=\\\")(.*?)(?=\\\")" | tail -1)', ' if [ "$ID" = "$1" ]; then', ' dbus-send --session --type=method_call --dest="$BUS" "$OBJ" org.kde.StatusNotifierItem.ContextMenu int32:"$2" int32:"$3"', ' exit 0', ' fi', 'done <<< "$ITEMS"',].join("\n");
|
const script = ['ITEMS=$(dbus-send --session --print-reply --dest=org.kde.StatusNotifierWatcher /StatusNotifierWatcher org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierWatcher string:RegisteredStatusNotifierItems 2>/dev/null)', 'while IFS= read -r line; do', ' line="${line#*\\\"}"', ' line="${line%\\\"*}"', ' [ -z "$line" ] && continue', ' BUS="${line%%/*}"', ' OBJ="/${line#*/}"', ' ID=$(dbus-send --session --print-reply --dest="$BUS" "$OBJ" org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierItem string:Id 2>/dev/null | grep -oP "(?<=\\\")(.*?)(?=\\\")" | tail -1)', ' if [ "$ID" = "$1" ]; then', ' dbus-send --session --type=method_call --dest="$BUS" "$OBJ" org.kde.StatusNotifierItem.ContextMenu int32:"$2" int32:"$3"', ' exit 0', ' fi', 'done <<< "$ITEMS"',].join("\n");
|
||||||
@@ -164,6 +215,7 @@ BasePill {
|
|||||||
property int dropTargetIndex: -1
|
property int dropTargetIndex: -1
|
||||||
property bool suppressShiftAnimation: false
|
property bool suppressShiftAnimation: false
|
||||||
readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length
|
readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length
|
||||||
|
readonly property bool inlineExpanded: hasHiddenItems && !useOverflowPopup && menuOpen
|
||||||
visible: allTrayItems.length > 0
|
visible: allTrayItems.length > 0
|
||||||
opacity: allTrayItems.length > 0 ? 1 : 0
|
opacity: allTrayItems.length > 0 ? 1 : 0
|
||||||
|
|
||||||
|
|||||||
@@ -29,12 +29,6 @@ Singleton {
|
|||||||
property bool use24HourClock: true
|
property bool use24HourClock: true
|
||||||
property bool showSeconds: false
|
property bool showSeconds: false
|
||||||
property bool padHours12Hour: false
|
property bool padHours12Hour: false
|
||||||
property bool greeterUse24HourClock: true
|
|
||||||
property bool greeterShowSeconds: false
|
|
||||||
property bool greeterPadHours12Hour: false
|
|
||||||
property string greeterLockDateFormat: ""
|
|
||||||
property string greeterFontFamily: ""
|
|
||||||
property string greeterWallpaperFillMode: ""
|
|
||||||
property bool useFahrenheit: false
|
property bool useFahrenheit: false
|
||||||
property bool nightModeEnabled: false
|
property bool nightModeEnabled: false
|
||||||
property string weatherLocation: "New York, NY"
|
property string weatherLocation: "New York, NY"
|
||||||
@@ -87,12 +81,6 @@ Singleton {
|
|||||||
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
|
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
|
||||||
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
|
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
|
||||||
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
|
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
|
||||||
greeterUse24HourClock = settings.greeterUse24HourClock !== undefined ? settings.greeterUse24HourClock : use24HourClock;
|
|
||||||
greeterShowSeconds = settings.greeterShowSeconds !== undefined ? settings.greeterShowSeconds : showSeconds;
|
|
||||||
greeterPadHours12Hour = settings.greeterPadHours12Hour !== undefined ? settings.greeterPadHours12Hour : padHours12Hour;
|
|
||||||
greeterLockDateFormat = settings.greeterLockDateFormat !== undefined ? settings.greeterLockDateFormat : "";
|
|
||||||
greeterFontFamily = settings.greeterFontFamily !== undefined ? settings.greeterFontFamily : "";
|
|
||||||
greeterWallpaperFillMode = settings.greeterWallpaperFillMode !== undefined ? settings.greeterWallpaperFillMode : "";
|
|
||||||
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
|
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
|
||||||
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
|
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
|
||||||
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
|
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
|
||||||
@@ -149,27 +137,23 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getEffectiveTimeFormat() {
|
function getEffectiveTimeFormat() {
|
||||||
const use24 = greeterUse24HourClock;
|
if (use24HourClock)
|
||||||
const secs = greeterShowSeconds;
|
return showSeconds ? "hh:mm:ss" : "hh:mm";
|
||||||
const pad = greeterPadHours12Hour;
|
if (padHours12Hour)
|
||||||
if (use24)
|
return showSeconds ? "hh:mm:ss AP" : "hh:mm AP";
|
||||||
return secs ? "hh:mm:ss" : "hh:mm";
|
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
|
||||||
if (pad)
|
|
||||||
return secs ? "hh:mm:ss AP" : "hh:mm AP";
|
|
||||||
return secs ? "h:mm:ss AP" : "h:mm AP";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEffectiveLockDateFormat() {
|
function getEffectiveLockDateFormat() {
|
||||||
const fmt = (greeterLockDateFormat !== undefined && greeterLockDateFormat !== "") ? greeterLockDateFormat : lockDateFormat;
|
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat;
|
||||||
return fmt && fmt.length > 0 ? fmt : Locale.LongFormat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEffectiveWallpaperFillMode() {
|
function getEffectiveWallpaperFillMode() {
|
||||||
return (greeterWallpaperFillMode && greeterWallpaperFillMode !== "") ? greeterWallpaperFillMode : wallpaperFillMode;
|
return wallpaperFillMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEffectiveFontFamily() {
|
function getEffectiveFontFamily() {
|
||||||
return (greeterFontFamily && greeterFontFamily !== "") ? greeterFontFamily : fontFamily;
|
return fontFamily;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilteredScreens(componentId) {
|
function getFilteredScreens(componentId) {
|
||||||
|
|||||||
@@ -147,6 +147,13 @@ Scope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Pam {
|
||||||
|
id: sharedPam
|
||||||
|
lockSecured: root.shouldLock
|
||||||
|
buffer: root.sharedPasswordBuffer
|
||||||
|
onUnlockRequested: root.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
WlSessionLock {
|
WlSessionLock {
|
||||||
id: sessionLock
|
id: sessionLock
|
||||||
|
|
||||||
@@ -170,6 +177,7 @@ Scope {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: lockSurface.isActiveScreen
|
visible: lockSurface.isActiveScreen
|
||||||
lock: sessionLock
|
lock: sessionLock
|
||||||
|
pam: sharedPam
|
||||||
sharedPasswordBuffer: root.sharedPasswordBuffer
|
sharedPasswordBuffer: root.sharedPasswordBuffer
|
||||||
screenName: lockSurface.currentScreenName
|
screenName: lockSurface.currentScreenName
|
||||||
isLocked: shouldLock
|
isLocked: shouldLock
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Item {
|
|||||||
|
|
||||||
property string passwordBuffer: ""
|
property string passwordBuffer: ""
|
||||||
property bool demoMode: false
|
property bool demoMode: false
|
||||||
|
property var pam: demoPam
|
||||||
property string screenName: ""
|
property string screenName: ""
|
||||||
property bool unlocking: false
|
property bool unlocking: false
|
||||||
property string pamState: ""
|
property string pamState: ""
|
||||||
@@ -52,20 +53,18 @@ Item {
|
|||||||
return I18n.tr("Touch your security key...");
|
return I18n.tr("Touch your security key...");
|
||||||
if (pam.lockMessage && pam.lockMessage.length > 0)
|
if (pam.lockMessage && pam.lockMessage.length > 0)
|
||||||
return pam.lockMessage;
|
return pam.lockMessage;
|
||||||
if (pam.fprintState === "error") {
|
|
||||||
const detail = (pam.fprint.message || "").trim();
|
|
||||||
return detail.length > 0 ? I18n.tr("Fingerprint error: %1").arg(detail) : I18n.tr("Fingerprint error");
|
|
||||||
}
|
|
||||||
if (pam.fprintState === "max")
|
|
||||||
return I18n.tr("Maximum fingerprint attempts reached. Please use password.");
|
|
||||||
if (pam.fprintState === "fail")
|
|
||||||
return I18n.tr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(SettingsData.maxFprintTries);
|
|
||||||
if (root.pamState === "error")
|
if (root.pamState === "error")
|
||||||
return I18n.tr("Authentication error - try again");
|
return I18n.tr("Authentication error - try again");
|
||||||
if (root.pamState === "max")
|
if (root.pamState === "max")
|
||||||
return I18n.tr("Too many attempts - locked out");
|
return I18n.tr("Too many attempts - locked out");
|
||||||
if (root.pamState === "fail")
|
if (root.pamState === "fail")
|
||||||
return I18n.tr("Incorrect password - try again");
|
return I18n.tr("Incorrect password - try again");
|
||||||
|
if (pam.fprintState === "error")
|
||||||
|
return I18n.tr("Fingerprint error");
|
||||||
|
if (pam.fprintState === "max")
|
||||||
|
return I18n.tr("Maximum fingerprint attempts reached. Please use password.");
|
||||||
|
if (pam.fprintState === "fail")
|
||||||
|
return I18n.tr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(SettingsData.maxFprintTries);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,13 +744,6 @@ Item {
|
|||||||
easing.type: Theme.standardEasing
|
easing.type: Theme.standardEasing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1639,49 +1631,46 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Pam {
|
Pam {
|
||||||
id: pam
|
id: demoPam
|
||||||
lockSecured: !demoMode
|
lockSecured: false
|
||||||
onUnlockRequested: {
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.pam
|
||||||
|
|
||||||
|
function onUnlockRequested() {
|
||||||
root.unlocking = true;
|
root.unlocking = true;
|
||||||
lockerReadyArmed = false;
|
lockerReadyArmed = false;
|
||||||
passwordField.text = "";
|
passwordField.text = "";
|
||||||
root.passwordBuffer = "";
|
root.passwordBuffer = "";
|
||||||
root.unlockRequested();
|
root.unlockRequested();
|
||||||
}
|
}
|
||||||
onStateChanged: {
|
|
||||||
root.pamState = state;
|
|
||||||
if (state !== "") {
|
|
||||||
root.unlocking = false;
|
|
||||||
placeholderDelay.restart();
|
|
||||||
passwordField.text = "";
|
|
||||||
root.passwordBuffer = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onU2fPendingChanged: {
|
|
||||||
if (u2fPending) {
|
|
||||||
passwordField.text = "";
|
|
||||||
root.passwordBuffer = "";
|
|
||||||
if (keyboardController.isKeyboardActive)
|
|
||||||
keyboardController.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
function onStateChanged() {
|
||||||
target: pam
|
root.pamState = root.pam.state;
|
||||||
|
if (root.pam.state === "")
|
||||||
|
return;
|
||||||
|
root.unlocking = false;
|
||||||
|
placeholderDelay.restart();
|
||||||
|
passwordField.text = "";
|
||||||
|
root.passwordBuffer = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function onU2fPendingChanged() {
|
||||||
|
if (!root.pam.u2fPending)
|
||||||
|
return;
|
||||||
|
passwordField.text = "";
|
||||||
|
root.passwordBuffer = "";
|
||||||
|
if (keyboardController.isKeyboardActive)
|
||||||
|
keyboardController.hide();
|
||||||
|
}
|
||||||
|
|
||||||
function onUnlockInProgressChanged() {
|
function onUnlockInProgressChanged() {
|
||||||
if (!pam.unlockInProgress && root.unlocking)
|
if (!root.pam.unlockInProgress && root.unlocking)
|
||||||
root.unlocking = false;
|
root.unlocking = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Binding {
|
|
||||||
target: pam
|
|
||||||
property: "buffer"
|
|
||||||
value: root.passwordBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: placeholderDelay
|
id: placeholderDelay
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Rectangle {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property WlSessionLock lock
|
required property WlSessionLock lock
|
||||||
|
required property var pam
|
||||||
required property string sharedPasswordBuffer
|
required property string sharedPasswordBuffer
|
||||||
required property string screenName
|
required property string screenName
|
||||||
required property bool isLocked
|
required property bool isLocked
|
||||||
@@ -21,6 +22,7 @@ Rectangle {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
demoMode: false
|
demoMode: false
|
||||||
|
pam: root.pam
|
||||||
passwordBuffer: root.sharedPasswordBuffer
|
passwordBuffer: root.sharedPasswordBuffer
|
||||||
screenName: root.screenName
|
screenName: root.screenName
|
||||||
onUnlockRequested: root.unlockRequested()
|
onUnlockRequested: root.unlockRequested()
|
||||||
|
|||||||
@@ -179,6 +179,8 @@ Scope {
|
|||||||
abort();
|
abort();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (active)
|
||||||
|
return;
|
||||||
|
|
||||||
tries = 0;
|
tries = 0;
|
||||||
errorTries = 0;
|
errorTries = 0;
|
||||||
@@ -192,22 +194,23 @@ Scope {
|
|||||||
if (!available)
|
if (!available)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (res === PamResult.Success) {
|
switch (res) {
|
||||||
|
case PamResult.Success:
|
||||||
if (!root.unlockInProgress) {
|
if (!root.unlockInProgress) {
|
||||||
passwd.abort();
|
passwd.abort();
|
||||||
root.proceedAfterPrimaryAuth();
|
root.proceedAfterPrimaryAuth();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
case PamResult.Error:
|
||||||
|
|
||||||
if (res === PamResult.Error) {
|
|
||||||
root.fprintState = "error";
|
|
||||||
errorTries++;
|
errorTries++;
|
||||||
if (errorTries < 5) {
|
if (errorTries < 200) {
|
||||||
abort();
|
abort();
|
||||||
errorRetry.restart();
|
errorRetry.restart();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else if (res === PamResult.MaxTries) {
|
abort();
|
||||||
|
return;
|
||||||
|
case PamResult.MaxTries:
|
||||||
tries++;
|
tries++;
|
||||||
if (tries < SettingsData.maxFprintTries) {
|
if (tries < SettingsData.maxFprintTries) {
|
||||||
root.fprintState = "fail";
|
root.fprintState = "fail";
|
||||||
@@ -216,6 +219,9 @@ Scope {
|
|||||||
root.fprintState = "max";
|
root.fprintState = "max";
|
||||||
abort();
|
abort();
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
root.flashMsg();
|
root.flashMsg();
|
||||||
@@ -294,7 +300,7 @@ Scope {
|
|||||||
Timer {
|
Timer {
|
||||||
id: errorRetry
|
id: errorRetry
|
||||||
|
|
||||||
interval: 800
|
interval: 1500
|
||||||
onTriggered: fprint.start()
|
onTriggered: fprint.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,26 +352,22 @@ Scope {
|
|||||||
id: fprintStateReset
|
id: fprintStateReset
|
||||||
|
|
||||||
interval: 4000
|
interval: 4000
|
||||||
onTriggered: {
|
onTriggered: root.fprintState = ""
|
||||||
root.fprintState = "";
|
|
||||||
fprint.errorTries = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onLockSecuredChanged: {
|
onLockSecuredChanged: {
|
||||||
if (lockSecured) {
|
if (!lockSecured) {
|
||||||
SettingsData.refreshAuthAvailability();
|
|
||||||
root.state = "";
|
|
||||||
root.fprintState = "";
|
|
||||||
root.u2fState = "";
|
|
||||||
root.u2fPending = false;
|
|
||||||
root.lockMessage = "";
|
|
||||||
root.resetAuthFlows();
|
|
||||||
fprint.checkAvail();
|
|
||||||
u2f.checkAvail();
|
|
||||||
} else {
|
|
||||||
root.resetAuthFlows();
|
root.resetAuthFlows();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
root.state = "";
|
||||||
|
root.fprintState = "";
|
||||||
|
root.u2fState = "";
|
||||||
|
root.u2fPending = false;
|
||||||
|
root.lockMessage = "";
|
||||||
|
root.resetAuthFlows();
|
||||||
|
fprint.checkAvail();
|
||||||
|
u2f.checkAvail();
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
|||||||
@@ -674,8 +674,10 @@ Rectangle {
|
|||||||
onEntered: parent.isHovered = true
|
onEntered: parent.isHovered = true
|
||||||
onExited: parent.isHovered = false
|
onExited: parent.isHovered = false
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (modelData && modelData.invoke)
|
if (modelData && modelData.invoke) {
|
||||||
modelData.invoke();
|
modelData.invoke();
|
||||||
|
PopoutService.closeNotificationCenter();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -813,6 +815,7 @@ Rectangle {
|
|||||||
onClicked: {
|
onClicked: {
|
||||||
if (modelData && modelData.invoke) {
|
if (modelData && modelData.invoke) {
|
||||||
modelData.invoke();
|
modelData.invoke();
|
||||||
|
PopoutService.closeNotificationCenter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ DankPopout {
|
|||||||
onDprChanged: updateStablePopupHeight()
|
onDprChanged: updateStablePopupHeight()
|
||||||
|
|
||||||
onShouldBeVisibleChanged: {
|
onShouldBeVisibleChanged: {
|
||||||
|
notificationHistoryVisible = shouldBeVisible;
|
||||||
|
|
||||||
if (shouldBeVisible) {
|
if (shouldBeVisible) {
|
||||||
NotificationService.onOverlayOpen();
|
NotificationService.onOverlayOpen();
|
||||||
updateStablePopupHeight();
|
updateStablePopupHeight();
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ DankOSD {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property bool _pendingShow: false
|
property bool _pendingShow: false
|
||||||
|
property string _displayTitle: ""
|
||||||
|
property string _displayArtist: ""
|
||||||
|
property string _displayAlbum: ""
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: iconDebounce
|
id: iconDebounce
|
||||||
@@ -105,6 +108,12 @@ DankOSD {
|
|||||||
return;
|
return;
|
||||||
if (!SettingsData.osdMediaPlaybackEnabled)
|
if (!SettingsData.osdMediaPlaybackEnabled)
|
||||||
return;
|
return;
|
||||||
|
if (MprisController.isFirefoxYoutubeHoverPreview(player))
|
||||||
|
return;
|
||||||
|
|
||||||
|
root._displayTitle = player.trackTitle || "";
|
||||||
|
root._displayArtist = player.trackArtist || "";
|
||||||
|
root._displayAlbum = player.trackAlbum || "";
|
||||||
|
|
||||||
root.updatePlaybackIcon();
|
root.updatePlaybackIcon();
|
||||||
TrackArtService.loadArtwork(player.trackArtUrl);
|
TrackArtService.loadArtwork(player.trackArtUrl);
|
||||||
@@ -254,7 +263,7 @@ DankOSD {
|
|||||||
StyledText {
|
StyledText {
|
||||||
id: topText
|
id: topText
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: player ? `${player.trackTitle || I18n.tr("Unknown Title")}` : ""
|
text: player ? (root._displayTitle || I18n.tr("Unknown Title")) : ""
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
@@ -265,7 +274,7 @@ DankOSD {
|
|||||||
StyledText {
|
StyledText {
|
||||||
id: bottomText
|
id: bottomText
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: player ? ((player.trackArtist || I18n.tr("Unknown Artist")) + (player.trackAlbum ? ` • ${player.trackAlbum}` : "")) : ""
|
text: player ? ((root._displayArtist || I18n.tr("Unknown Artist")) + (root._displayAlbum ? ` • ${root._displayAlbum}` : "")) : ""
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
font.weight: Font.Light
|
font.weight: Font.Light
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ Item {
|
|||||||
property real minWidth: contentLoader.item?.minWidth ?? 100
|
property real minWidth: contentLoader.item?.minWidth ?? 100
|
||||||
property real minHeight: contentLoader.item?.minHeight ?? 100
|
property real minHeight: contentLoader.item?.minHeight ?? 100
|
||||||
property bool forceSquare: contentLoader.item?.forceSquare ?? false
|
property bool forceSquare: contentLoader.item?.forceSquare ?? false
|
||||||
|
property bool acceptsKeyboardFocus: contentLoader.item?.acceptsKeyboardFocus ?? false
|
||||||
property bool isInteracting: dragArea.pressed || resizeArea.pressed
|
property bool isInteracting: dragArea.pressed || resizeArea.pressed
|
||||||
|
|
||||||
property var _gridSettingsTrigger: SettingsData.desktopWidgetGridSettings
|
property var _gridSettingsTrigger: SettingsData.desktopWidgetGridSettings
|
||||||
@@ -299,11 +300,14 @@ Item {
|
|||||||
}
|
}
|
||||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||||
WlrLayershell.keyboardFocus: {
|
WlrLayershell.keyboardFocus: {
|
||||||
if (!root.isInteracting)
|
if (root.isInteracting) {
|
||||||
return WlrKeyboardFocus.None;
|
if (CompositorService.useHyprlandFocusGrab)
|
||||||
if (CompositorService.useHyprlandFocusGrab)
|
return WlrKeyboardFocus.OnDemand;
|
||||||
|
return WlrKeyboardFocus.Exclusive;
|
||||||
|
}
|
||||||
|
if (root.acceptsKeyboardFocus)
|
||||||
return WlrKeyboardFocus.OnDemand;
|
return WlrKeyboardFocus.OnDemand;
|
||||||
return WlrKeyboardFocus.Exclusive;
|
return WlrKeyboardFocus.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
HyprlandFocusGrab {
|
HyprlandFocusGrab {
|
||||||
|
|||||||
112
quickshell/Modules/Settings/MuxTab.qml
Normal file
112
quickshell/Modules/Settings/MuxTab.qml
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.Settings.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property var muxTypeOptions: [
|
||||||
|
"tmux",
|
||||||
|
"zellij"
|
||||||
|
]
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
contentHeight: mainColumn.height + Theme.spacingXL
|
||||||
|
contentWidth: width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
topPadding: 4
|
||||||
|
width: Math.min(550, parent.width - Theme.spacingL * 2)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
tab: "mux"
|
||||||
|
tags: ["mux", "multiplexer", "tmux", "zellij", "type"]
|
||||||
|
title: I18n.tr("Multiplexer")
|
||||||
|
iconName: "terminal"
|
||||||
|
|
||||||
|
SettingsDropdownRow {
|
||||||
|
tab: "mux"
|
||||||
|
tags: ["mux", "multiplexer", "tmux", "zellij", "type", "backend"]
|
||||||
|
settingKey: "muxType"
|
||||||
|
text: I18n.tr("Multiplexer Type")
|
||||||
|
description: I18n.tr("Terminal multiplexer backend to use")
|
||||||
|
options: root.muxTypeOptions
|
||||||
|
currentValue: SettingsData.muxType
|
||||||
|
onValueChanged: value => SettingsData.set("muxType", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
tab: "mux"
|
||||||
|
tags: ["mux", "terminal", "custom", "command", "script"]
|
||||||
|
title: I18n.tr("Terminal")
|
||||||
|
iconName: "desktop_windows"
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tab: "mux"
|
||||||
|
tags: ["mux", "custom", "command", "override"]
|
||||||
|
settingKey: "muxUseCustomCommand"
|
||||||
|
text: I18n.tr("Use Custom Command")
|
||||||
|
description: I18n.tr("Override terminal with a custom command or script")
|
||||||
|
checked: SettingsData.muxUseCustomCommand
|
||||||
|
onToggled: checked => SettingsData.set("muxUseCustomCommand", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent?.width ?? 0
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: SettingsData.muxUseCustomCommand
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("The custom command used when attaching to sessions (receives the session name as the first argument)")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
width: parent.width
|
||||||
|
text: SettingsData.muxCustomCommand
|
||||||
|
placeholderText: I18n.tr("Enter command or script path")
|
||||||
|
onTextEdited: SettingsData.set("muxCustomCommand", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
tab: "mux"
|
||||||
|
tags: ["mux", "session", "filter", "exclude", "hide"]
|
||||||
|
title: I18n.tr("Session Filter")
|
||||||
|
iconName: "filter_list"
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent?.width ?? 0
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("Comma-separated list of session names to hide. Wrap in slashes for regex (e.g., /^_.*/).")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
width: parent.width
|
||||||
|
text: SettingsData.muxSessionFilter
|
||||||
|
placeholderText: I18n.tr("e.g., scratch, /^tmp_.*/, build")
|
||||||
|
onTextEdited: SettingsData.set("muxSessionFilter", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,6 +68,20 @@ Scope {
|
|||||||
hideSpotlight();
|
hideSpotlight();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onIsClosingChanged: {
|
||||||
|
if (!isClosing) {
|
||||||
|
closeTimer.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: closeTimer
|
||||||
|
interval: Theme.expressiveDurations.fast
|
||||||
|
onTriggered: niriOverviewScope.resetState()
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: niriOverlayLoader
|
id: niriOverlayLoader
|
||||||
active: overlayActive || isClosing
|
active: overlayActive || isClosing
|
||||||
@@ -128,7 +142,7 @@ Scope {
|
|||||||
WindowBlur {
|
WindowBlur {
|
||||||
targetWindow: overlayWindow
|
targetWindow: overlayWindow
|
||||||
readonly property real s: Math.min(1, spotlightContainer.scale)
|
readonly property real s: Math.min(1, spotlightContainer.scale)
|
||||||
readonly property bool active: spotlightContainer.visible && spotlightContainer.opacity > 0
|
readonly property bool active: overlayWindow.shouldShowSpotlight && spotlightContainer.opacity > 0
|
||||||
blurX: spotlightContainer.x + spotlightContainer.width * (1 - s) * 0.5
|
blurX: spotlightContainer.x + spotlightContainer.width * (1 - s) * 0.5
|
||||||
blurY: spotlightContainer.y + spotlightContainer.height * (1 - s) * 0.5
|
blurY: spotlightContainer.y + spotlightContainer.height * (1 - s) * 0.5
|
||||||
blurWidth: active ? spotlightContainer.width * s : 0
|
blurWidth: active ? spotlightContainer.width * s : 0
|
||||||
@@ -256,16 +270,10 @@ Scope {
|
|||||||
layer.textureSize: layer.enabled ? Qt.size(Math.round(width * overlayWindow.dpr), Math.round(height * overlayWindow.dpr)) : Qt.size(0, 0)
|
layer.textureSize: layer.enabled ? Qt.size(Math.round(width * overlayWindow.dpr), Math.round(height * overlayWindow.dpr)) : Qt.size(0, 0)
|
||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
id: scaleAnimation
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.fast
|
duration: Theme.expressiveDurations.fast
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||||
onRunningChanged: {
|
|
||||||
if (running || !spotlightContainer.animatingOut)
|
|
||||||
return;
|
|
||||||
niriOverviewScope.resetState();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,36 @@ Singleton {
|
|||||||
return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream && !hidden.includes(node.name));
|
return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream && !hidden.includes(node.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve a PwNode by name from the live typed list and assign it as the
|
||||||
|
// default sink. Going through Pipewire.nodes.values directly (no .filter
|
||||||
|
// / spread / .sort / property var) avoids QML type erasure to QObject*,
|
||||||
|
// which newer quickshell rejects when assigning to preferredDefaultAudioSink.
|
||||||
|
function setDefaultSinkByName(name) {
|
||||||
|
if (!name)
|
||||||
|
return false;
|
||||||
|
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||||
|
const node = Pipewire.nodes.values[i];
|
||||||
|
if (node && node.name === name && node.audio && node.isSink && !node.isStream) {
|
||||||
|
Pipewire.preferredDefaultAudioSink = node;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultSourceByName(name) {
|
||||||
|
if (!name)
|
||||||
|
return false;
|
||||||
|
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||||
|
const node = Pipewire.nodes.values[i];
|
||||||
|
if (node && node.name === name && node.audio && !node.isSink && !node.isStream) {
|
||||||
|
Pipewire.preferredDefaultAudioSource = node;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function cycleAudioOutput() {
|
function cycleAudioOutput() {
|
||||||
const sinks = getAvailableSinks();
|
const sinks = getAvailableSinks();
|
||||||
if (sinks.length < 2)
|
if (sinks.length < 2)
|
||||||
@@ -80,7 +110,8 @@ Singleton {
|
|||||||
const currentIndex = sinks.findIndex(s => s.name === currentName);
|
const currentIndex = sinks.findIndex(s => s.name === currentName);
|
||||||
const nextIndex = (currentIndex + 1) % sinks.length;
|
const nextIndex = (currentIndex + 1) % sinks.length;
|
||||||
const nextSink = sinks[nextIndex];
|
const nextSink = sinks[nextIndex];
|
||||||
Pipewire.preferredDefaultAudioSink = nextSink;
|
if (!setDefaultSinkByName(nextSink.name))
|
||||||
|
Pipewire.preferredDefaultAudioSink = nextSink;
|
||||||
const name = displayName(nextSink);
|
const name = displayName(nextSink);
|
||||||
audioOutputCycled(name, sinkIcon(nextSink));
|
audioOutputCycled(name, sinkIcon(nextSink));
|
||||||
return name;
|
return name;
|
||||||
|
|||||||
@@ -10,4 +10,14 @@ Singleton {
|
|||||||
|
|
||||||
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
|
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
|
||||||
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null
|
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null
|
||||||
|
|
||||||
|
function isFirefoxYoutubeHoverPreview(player: MprisPlayer): bool {
|
||||||
|
if (!player)
|
||||||
|
return false;
|
||||||
|
const id = (player.identity || "").toLowerCase();
|
||||||
|
if (!id.includes("firefox"))
|
||||||
|
return false;
|
||||||
|
const url = (player.metadata?.["xesam:url"] || "").toString();
|
||||||
|
return /^https?:\/\/(www\.)?youtube\.com\/?($|\?|#)/i.test(url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
230
quickshell/Services/MuxService.qml
Normal file
230
quickshell/Services/MuxService.qml
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var sessions: []
|
||||||
|
property bool loading: false
|
||||||
|
|
||||||
|
property bool tmuxAvailable: false
|
||||||
|
property bool zellijAvailable: false
|
||||||
|
readonly property bool currentMuxAvailable: muxType === "zellij" ? zellijAvailable : tmuxAvailable
|
||||||
|
|
||||||
|
readonly property string muxType: SettingsData.muxType
|
||||||
|
readonly property string displayName: muxType === "zellij" ? "Zellij" : "Tmux"
|
||||||
|
|
||||||
|
readonly property var terminalFlags: ({
|
||||||
|
"ghostty": ["-e"],
|
||||||
|
"kitty": ["-e"],
|
||||||
|
"alacritty": ["-e"],
|
||||||
|
"foot": [],
|
||||||
|
"wezterm": ["start", "--"],
|
||||||
|
"gnome-terminal": ["--"],
|
||||||
|
"xterm": ["-e"],
|
||||||
|
"konsole": ["-e"],
|
||||||
|
"st": ["-e"],
|
||||||
|
"terminator": ["-e"],
|
||||||
|
"xfce4-terminal": ["-e"]
|
||||||
|
})
|
||||||
|
|
||||||
|
function getTerminalFlag(terminal) {
|
||||||
|
return terminalFlags[terminal] ?? ["-e"]
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property string terminal: Quickshell.env("TERMINAL") || "ghostty"
|
||||||
|
|
||||||
|
function _terminalPrefix() {
|
||||||
|
return [terminal].concat(getTerminalFlag(terminal))
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: tmuxCheckProcess
|
||||||
|
command: ["which", "tmux"]
|
||||||
|
running: false
|
||||||
|
onExited: (code) => { root.tmuxAvailable = (code === 0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: zellijCheckProcess
|
||||||
|
command: ["which", "zellij"]
|
||||||
|
running: false
|
||||||
|
onExited: (code) => { root.zellijAvailable = (code === 0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAvailability() {
|
||||||
|
tmuxCheckProcess.running = true
|
||||||
|
zellijCheckProcess.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: checkAvailability()
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: listProcess
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
if (root.muxType === "zellij")
|
||||||
|
root._parseZellijSessions(text)
|
||||||
|
else
|
||||||
|
root._parseTmuxSessions(text)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[MuxService] Error parsing sessions:", e)
|
||||||
|
root.sessions = []
|
||||||
|
}
|
||||||
|
root.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr: SplitParser {
|
||||||
|
onRead: (line) => {
|
||||||
|
if (line.trim())
|
||||||
|
console.error("[MuxService] stderr:", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: (code) => {
|
||||||
|
if (code !== 0 && code !== 1) {
|
||||||
|
console.warn("[MuxService] Process exited with code:", code)
|
||||||
|
root.sessions = []
|
||||||
|
}
|
||||||
|
root.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshSessions() {
|
||||||
|
if (!root.currentMuxAvailable) {
|
||||||
|
root.sessions = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
root.loading = true
|
||||||
|
|
||||||
|
if (listProcess.running)
|
||||||
|
listProcess.running = false
|
||||||
|
|
||||||
|
if (root.muxType === "zellij")
|
||||||
|
listProcess.command = ["zellij", "list-sessions", "--no-formatting"]
|
||||||
|
else
|
||||||
|
listProcess.command = ["tmux", "list-sessions", "-F", "#{session_name}|#{session_windows}|#{session_attached}"]
|
||||||
|
|
||||||
|
Qt.callLater(function () {
|
||||||
|
listProcess.running = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isSessionExcluded(name) {
|
||||||
|
var filter = SettingsData.muxSessionFilter.trim()
|
||||||
|
if (filter.length === 0)
|
||||||
|
return false
|
||||||
|
var parts = filter.split(",")
|
||||||
|
for (var i = 0; i < parts.length; i++) {
|
||||||
|
var pattern = parts[i].trim()
|
||||||
|
if (pattern.length === 0)
|
||||||
|
continue
|
||||||
|
if (pattern.startsWith("/") && pattern.endsWith("/") && pattern.length > 2) {
|
||||||
|
try {
|
||||||
|
var re = new RegExp(pattern.slice(1, -1))
|
||||||
|
if (re.test(name))
|
||||||
|
return true
|
||||||
|
} catch (e) {}
|
||||||
|
} else {
|
||||||
|
if (name.toLowerCase() === pattern.toLowerCase())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function _parseTmuxSessions(output) {
|
||||||
|
var sessionList = []
|
||||||
|
var lines = output.trim().split('\n')
|
||||||
|
|
||||||
|
for (var i = 0; i < lines.length; i++) {
|
||||||
|
var line = lines[i].trim()
|
||||||
|
if (line.length === 0)
|
||||||
|
continue
|
||||||
|
|
||||||
|
var parts = line.split('|')
|
||||||
|
if (parts.length >= 3 && !_isSessionExcluded(parts[0])) {
|
||||||
|
sessionList.push({
|
||||||
|
name: parts[0],
|
||||||
|
windows: parts[1],
|
||||||
|
attached: parts[2] === "1"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.sessions = sessionList
|
||||||
|
}
|
||||||
|
|
||||||
|
function _parseZellijSessions(output) {
|
||||||
|
var sessionList = []
|
||||||
|
var lines = output.trim().split('\n')
|
||||||
|
|
||||||
|
for (var i = 0; i < lines.length; i++) {
|
||||||
|
var line = lines[i].trim()
|
||||||
|
if (line.length === 0)
|
||||||
|
continue
|
||||||
|
|
||||||
|
var exited = line.includes("(EXITED")
|
||||||
|
var bracketIdx = line.indexOf(" [")
|
||||||
|
var name = (bracketIdx > 0 ? line.substring(0, bracketIdx) : line).trim()
|
||||||
|
|
||||||
|
if (!_isSessionExcluded(name)) {
|
||||||
|
sessionList.push({
|
||||||
|
name: name,
|
||||||
|
windows: "N/A",
|
||||||
|
attached: !exited
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.sessions = sessionList
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachToSession(name) {
|
||||||
|
if (SettingsData.muxUseCustomCommand && SettingsData.muxCustomCommand) {
|
||||||
|
Quickshell.execDetached([SettingsData.muxCustomCommand, name])
|
||||||
|
} else if (root.muxType === "zellij") {
|
||||||
|
Quickshell.execDetached(_terminalPrefix().concat(["zellij", "attach", name]))
|
||||||
|
} else {
|
||||||
|
Quickshell.execDetached(_terminalPrefix().concat(["tmux", "attach", "-t", name]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSession(name) {
|
||||||
|
if (SettingsData.muxUseCustomCommand && SettingsData.muxCustomCommand) {
|
||||||
|
Quickshell.execDetached([SettingsData.muxCustomCommand, name])
|
||||||
|
} else if (root.muxType === "zellij") {
|
||||||
|
Quickshell.execDetached(_terminalPrefix().concat(["zellij", "-s", name]))
|
||||||
|
} else {
|
||||||
|
Quickshell.execDetached(_terminalPrefix().concat(["tmux", "new-session", "-s", name]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property bool supportsRename: muxType !== "zellij"
|
||||||
|
|
||||||
|
function renameSession(oldName, newName) {
|
||||||
|
if (root.muxType === "zellij")
|
||||||
|
return
|
||||||
|
Quickshell.execDetached(["tmux", "rename-session", "-t", oldName, newName])
|
||||||
|
Qt.callLater(refreshSessions)
|
||||||
|
}
|
||||||
|
|
||||||
|
function killSession(name) {
|
||||||
|
if (root.muxType === "zellij") {
|
||||||
|
Quickshell.execDetached(["zellij", "kill-session", name])
|
||||||
|
} else {
|
||||||
|
Quickshell.execDetached(["tmux", "kill-session", "-t", name])
|
||||||
|
}
|
||||||
|
Qt.callLater(refreshSessions)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
v1.4.4
|
v1.4.6
|
||||||
|
|||||||
@@ -161,6 +161,13 @@ StyledRect {
|
|||||||
if (root.keyForwardTargets[i])
|
if (root.keyForwardTargets[i])
|
||||||
root.keyForwardTargets[i].Keys.pressed(event);
|
root.keyForwardTargets[i].Keys.pressed(event);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((event.modifiers & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier)) && root.keyForwardTargets.length > 0) {
|
||||||
|
for (var i = 0; i < root.keyForwardTargets.length; i++) {
|
||||||
|
if (root.keyForwardTargets[i])
|
||||||
|
root.keyForwardTargets[i].Keys.pressed(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#%PAM-1.0
|
#%PAM-1.0
|
||||||
|
|
||||||
auth required pam_fprintd.so max-tries=1 timeout=5
|
auth required pam_fprintd.so max-tries=5
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user