1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-02 11:38:30 -04:00

Compare commits

..

1 Commits

Author SHA1 Message Date
purian23 a95206bf41 fix(nix): support declarative greeter auto-login 2026-07-02 15:36:15 -04:00
346 changed files with 8477 additions and 22418 deletions
-8
View File
@@ -28,14 +28,6 @@ repos:
language: system
files: ^quickshell/(Modules/Settings/.*\.qml|Modals/Settings/SettingsSidebar\.qml|translations/extract_settings_index\.py)$
pass_filenames: false
- repo: local
hooks:
- id: i18n-term-freeze
name: i18n term freeze (no new I18n.tr/qsTr terms)
entry: python3 quickshell/translations/check_term_freeze.py
language: system
files: ^quickshell/(.*\.qml|translations/(term_freeze\.json|check_term_freeze\.py|extract_translations\.py))$
pass_filenames: false
- repo: local
hooks:
- id: no-console-in-qml
-2
View File
@@ -77,7 +77,6 @@ install-desktop:
@echo "Installing desktop entries..."
@install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop
@install -D -m 644 $(ASSETS_DIR)/com.danklinux.dms.desktop $(APPLICATIONS_DIR)/com.danklinux.dms.desktop
@install -D -m 644 $(ASSETS_DIR)/com.danklinux.dms.notepad.desktop $(APPLICATIONS_DIR)/com.danklinux.dms.notepad.desktop
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entries installed"
@@ -121,7 +120,6 @@ uninstall-desktop:
@echo "Removing desktop entries..."
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop
@rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.desktop
@rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.notepad.desktop
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entries removed"
-12
View File
@@ -1,12 +0,0 @@
[Desktop Entry]
Type=Application
Name=DMS Notepad
GenericName=Text Editor
Comment=Quick notes and text files in the dank desktop shell
Exec=dms ipc call notepad openFile %f
Icon=danklogo
Terminal=false
NoDisplay=true
Categories=Utility;TextEditor;
MimeType=text/plain;text/markdown;
StartupNotify=false
+2 -5
View File
@@ -10,7 +10,7 @@ Go-based backend for DankMaterialShell providing system integration, IPC, and in
Command-line interface and daemon for shell management and system control.
**dankinstall**
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, and Void. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
## System Integration
@@ -193,7 +193,7 @@ Set the `DANKINSTALL_LOG_DIR` environment variable to override the log directory
## Supported Distributions
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, Void (and derivatives)
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
**Arch Linux**
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
@@ -214,7 +214,4 @@ Most packages available in standard repos. Minimal building required.
**Gentoo**
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
**Void Linux**
Uses XBPS with the DMS and DankLinux self-hosted repositories.
See installer output for distribution-specific details during installation.
+1 -18
View File
@@ -63,15 +63,6 @@ var clipPasteCmd = &cobra.Command{
Run: runClipPaste,
}
var clipSendPasteCmd = &cobra.Command{
Use: "send-paste",
Short: "Send a paste keystroke to the focused window",
Long: "Emulate ctrl+v (or ctrl+shift+v with --shift) via a virtual keyboard. Works without server.",
Run: runClipSendPaste,
}
var clipSendPasteShift bool
var clipWatchCmd = &cobra.Command{
Use: "watch [command]",
Short: "Watch clipboard for changes",
@@ -225,10 +216,8 @@ func init() {
clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration")
clipSendPasteCmd.Flags().BoolVarP(&clipSendPasteShift, "shift", "s", false, "Send ctrl+shift+v (terminal paste)")
clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd)
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipSendPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
}
func runClipCopy(cmd *cobra.Command, args []string) {
@@ -325,12 +314,6 @@ func runClipPaste(cmd *cobra.Command, args []string) {
os.Stdout.Write(data)
}
func runClipSendPaste(cmd *cobra.Command, args []string) {
if err := clipboard.SendPasteKeystroke(clipSendPasteShift); err != nil {
log.Fatalf("send-paste: %v", err)
}
}
func runClipWatch(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
+5 -197
View File
@@ -106,8 +106,6 @@ func init() {
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
printIPCHelp()
})
pluginsUpdateCmd.Flags().BoolP("all", "a", false, "Update all installed plugins")
pluginsUpdateCmd.Flags().Bool("check", false, "Check for available updates without applying them")
}
var debugSrvCmd = &cobra.Command{
@@ -186,22 +184,10 @@ var pluginsUninstallCmd = &cobra.Command{
}
var pluginsUpdateCmd = &cobra.Command{
Use: "update [plugin-id]",
Short: "Update a plugin by ID, or all plugins",
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). If --all or -a is specified, all installed plugins will be updated.",
Args: func(cmd *cobra.Command, args []string) error {
updateAll, _ := cmd.Flags().GetBool("all")
if updateAll {
if len(args) > 0 {
return fmt.Errorf("cannot specify plugin ID when using --all/-a")
}
return nil
}
if len(args) != 1 {
return fmt.Errorf("requires exactly 1 arg (plugin ID) or use --all/-a")
}
return nil
},
Use: "update <plugin-id>",
Short: "Update a plugin by ID",
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). Plugin names are also supported.",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
@@ -209,26 +195,6 @@ var pluginsUpdateCmd = &cobra.Command{
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
checkOnly, _ := cmd.Flags().GetBool("check")
updateAll, _ := cmd.Flags().GetBool("all")
if checkOnly {
if updateAll {
if err := checkAllPluginsCLI(); err != nil {
log.Fatalf("Error checking updates: %v", err)
}
return
}
if err := checkPluginCLI(args[0]); err != nil {
log.Fatalf("Error checking update: %v", err)
}
return
}
if updateAll {
if err := updateAllPluginsCLI(); err != nil {
log.Fatalf("Error updating plugins: %v", err)
}
return
}
if err := updatePluginCLI(args[0]); err != nil {
log.Fatalf("Error updating plugin: %v", err)
}
@@ -404,11 +370,7 @@ func listInstalledPlugins() error {
fmt.Printf("\nInstalled Plugins (%d):\n\n", len(installedNames))
for _, id := range installedNames {
if plugin, ok := pluginMap[id]; ok {
hasUpdateStr := ""
if hasUpdates, _, err := manager.HasUpdates(id, plugin); err == nil && hasUpdates {
hasUpdateStr = " (update available)"
}
fmt.Printf(" %s%s\n", plugin.Name, hasUpdateStr)
fmt.Printf(" %s\n", plugin.Name)
fmt.Printf(" ID: %s\n", plugin.ID)
fmt.Printf(" Category: %s\n", plugin.Category)
fmt.Printf(" Author: %s\n", plugin.Author)
@@ -588,160 +550,6 @@ func updatePluginCLI(idOrName string) error {
return nil
}
func updateAllPluginsCLI() error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
installed, err := manager.ListInstalled()
if err != nil {
return fmt.Errorf("failed to list installed plugins: %w", err)
}
pluginList, _ := registry.List()
var errs []error
for _, pluginID := range installed {
plugin := plugins.FindByIDOrName(pluginID, pluginList)
if plugin != nil {
fmt.Printf("Updating plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
if err := manager.Update(*plugin); err != nil {
if strings.Contains(err.Error(), "cannot update system plugin") {
fmt.Printf("Skipping system plugin: %s\n", plugin.Name)
} else {
errs = append(errs, fmt.Errorf("failed to update %s: %w", plugin.Name, err))
}
} else {
fmt.Printf("Plugin updated successfully: %s\n", plugin.Name)
}
} else {
fmt.Printf("Updating plugin: %s\n", pluginID)
if err := manager.UpdateByIDOrName(pluginID); err != nil {
if strings.Contains(err.Error(), "cannot update system plugin") {
fmt.Printf("Skipping system plugin: %s\n", pluginID)
} else {
errs = append(errs, fmt.Errorf("failed to update %s: %w", pluginID, err))
}
} else {
fmt.Printf("Plugin updated successfully: %s\n", pluginID)
}
}
}
if len(errs) > 0 {
for _, err := range errs {
fmt.Fprintf(os.Stderr, "%v\n", err)
}
return fmt.Errorf("failed to update some plugins")
}
return nil
}
func checkPluginCLI(idOrName string) error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
pluginList, _ := registry.List()
plugin := plugins.FindByIDOrName(idOrName, pluginList)
if plugin != nil {
installed, err := manager.IsInstalled(*plugin)
if err != nil {
return fmt.Errorf("failed to check install status: %w", err)
}
if !installed {
return fmt.Errorf("plugin not installed: %s", plugin.Name)
}
hasUpdates, _, err := manager.HasUpdates(plugin.ID, *plugin)
if err != nil {
return fmt.Errorf("failed to check updates: %w", err)
}
if hasUpdates {
fmt.Printf("Update available for plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
} else {
fmt.Printf("Plugin is up to date: %s\n", plugin.Name)
}
return nil
}
dummyPlugin := plugins.Plugin{ID: idOrName}
hasUpdates, _, err := manager.HasUpdates(idOrName, dummyPlugin)
if err != nil {
return fmt.Errorf("failed to check updates: %w", err)
}
if hasUpdates {
fmt.Printf("Update available for plugin: %s\n", idOrName)
} else {
fmt.Printf("Plugin is up to date: %s\n", idOrName)
}
return nil
}
func checkAllPluginsCLI() error {
manager, err := plugins.NewManager()
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
registry, err := plugins.NewRegistry()
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
installed, err := manager.ListInstalled()
if err != nil {
return fmt.Errorf("failed to list installed plugins: %w", err)
}
pluginList, _ := registry.List()
var count int
for _, pluginID := range installed {
plugin := plugins.FindByIDOrName(pluginID, pluginList)
var hasUpdates bool
var name string
if plugin != nil {
name = plugin.Name
hasUpdates, _, _ = manager.HasUpdates(pluginID, *plugin)
} else {
name = pluginID
dummyPlugin := plugins.Plugin{ID: pluginID}
hasUpdates, _, _ = manager.HasUpdates(pluginID, dummyPlugin)
}
if hasUpdates {
fmt.Printf("Update available for plugin: %s (ID: %s)\n", name, pluginID)
count++
}
}
if count > 0 {
fmt.Printf("\nFound %d plugin(s) with available updates.\n", count)
} else {
fmt.Println("All plugins are up to date.")
}
return nil
}
func getCommonCommands() []*cobra.Command {
return []*cobra.Command{
versionCmd,
+1 -4
View File
@@ -1534,8 +1534,6 @@ func packageInstallHint() string {
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
case distros.FamilyArch:
return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'"
case distros.FamilyVoid:
return "Install with 'sudo xbps-install -S dms-greeter' (requires DMS XBPS repo: echo 'repository=https://avengemedia.github.io/DankMaterialShell/current' | sudo tee /etc/xbps.d/dms.conf)"
default:
return "Run 'dms greeter install' to install greeter"
}
@@ -1574,8 +1572,7 @@ func isPackageOnlyGreeterDistro() bool {
config.Family == distros.FamilySUSE ||
config.Family == distros.FamilyUbuntu ||
config.Family == distros.FamilyFedora ||
config.Family == distros.FamilyArch ||
config.Family == distros.FamilyVoid
config.Family == distros.FamilyArch
}
func promptCompositorChoice(compositors []string) (string, error) {
-23
View File
@@ -38,17 +38,10 @@ var matugenCheckCmd = &cobra.Command{
Run: runMatugenCheck,
}
var matugenPreviewCmd = &cobra.Command{
Use: "preview",
Short: "Preview Matugen scheme colors without applying them",
Run: runMatugenPreview,
}
func init() {
matugenCmd.AddCommand(matugenGenerateCmd)
matugenCmd.AddCommand(matugenQueueCmd)
matugenCmd.AddCommand(matugenCheckCmd)
matugenCmd.AddCommand(matugenPreviewCmd)
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
cmd.Flags().String("state-dir", "", "State directory for cache files")
@@ -69,8 +62,6 @@ func init() {
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
matugenPreviewCmd.Flags().String("source-color", "", "Source color used to generate previews")
matugenPreviewCmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
}
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
@@ -209,17 +200,3 @@ func runMatugenCheck(cmd *cobra.Command, args []string) {
}
fmt.Println(string(data))
}
func runMatugenPreview(cmd *cobra.Command, args []string) {
sourceColor, _ := cmd.Flags().GetString("source-color")
contrast, _ := cmd.Flags().GetFloat64("contrast")
previews, err := matugen.PreviewSchemes(sourceColor, contrast)
if err != nil {
log.Fatalf("Failed to generate Matugen previews: %v", err)
}
data, err := json.Marshal(previews)
if err != nil {
log.Fatalf("Failed to marshal Matugen previews: %v", err)
}
fmt.Println(string(data))
}
-13
View File
@@ -10,7 +10,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
@@ -299,9 +298,6 @@ func runSetup() error {
if wmSelected {
if wm == deps.WindowManagerMango {
useSystemd = false
} else if isVoidSetup() {
useSystemd = false
fmt.Println("\nVoid Linux detected; deploying non-systemd session config.")
} else {
useSystemd = promptSystemd()
}
@@ -376,15 +372,6 @@ func runSetup() error {
return nil
}
func isVoidSetup() bool {
osInfo, err := distros.GetOSInfo()
if err != nil {
return false
}
config, exists := distros.Registry[osInfo.Distribution.ID]
return exists && config.Family == distros.FamilyVoid
}
// 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() {
+2 -9
View File
@@ -582,11 +582,7 @@ func runShellDaemon(session bool) {
}
var qsHasAnyDisplay = sync.OnceValue(func() bool {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "qs", "ipc", "--help")
cmd.WaitDelay = 500 * time.Millisecond
out, err := cmd.Output()
out, err := exec.Command("qs", "ipc", "--help").Output()
if err != nil {
return false
}
@@ -646,10 +642,7 @@ func getShellIPCCompletions(args []string, _ string) []string {
return nil
}
cmdArgs := append(baseArgs, "show")
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "qs", cmdArgs...)
cmd.WaitDelay = 500 * time.Millisecond
cmd := exec.Command("qs", cmdArgs...)
var targets ipcTargets
if output, err := cmd.Output(); err == nil {
+316 -144
View File
@@ -9,6 +9,7 @@ import (
"syscall"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
const envServe = "_DMS_CLIPBOARD_SERVE"
@@ -16,37 +17,6 @@ const envMime = "_DMS_CLIPBOARD_MIME"
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
type Offer struct {
MimeType string
Data []byte
}
// textMimeAliases are offered alongside plain-text content so legacy X11
// clients bridged through XWayland find a target they can convert.
var textMimeAliases = []string{
"text/plain",
"text/plain;charset=utf-8",
"UTF8_STRING",
"STRING",
"TEXT",
}
// ExpandOffers turns raw clipboard data into the full offer list to serve,
// adding the standard alias set for text content.
func ExpandOffers(data []byte, mimeType string) []Offer {
offers := []Offer{{MimeType: mimeType, Data: data}}
if mimeType != "text/plain" && mimeType != "text/plain;charset=utf-8" {
return offers
}
for _, alias := range textMimeAliases {
if alias == mimeType {
continue
}
offers = append(offers, Offer{MimeType: alias, Data: data})
}
return offers
}
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
// child. Reads source data into memory, deletes any cache file, then serves.
func MaybeServeAndExit() {
@@ -74,7 +44,7 @@ func MaybeServeAndExit() {
os.Exit(1)
}
if err := serveOffers(ExpandOffers(data, mimeType), pasteOnce); err != nil {
if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
os.Exit(1)
}
@@ -85,33 +55,22 @@ func Copy(data []byte, mimeType string) error {
return copyForkCached(data, mimeType, false)
}
func CopyText(text string) error {
return Copy([]byte(text), "text/plain;charset=utf-8")
}
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
if foreground {
return serveOffers(ExpandOffers(data, mimeType), pasteOnce)
return serveClipboard(data, mimeType, pasteOnce)
}
return copyForkCached(data, mimeType, pasteOnce)
}
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
if !foreground {
return copyFork(data, mimeType, pasteOnce)
}
buf, err := io.ReadAll(data)
if err != nil {
return fmt.Errorf("read source: %w", err)
}
return serveOffers(ExpandOffers(buf, mimeType), pasteOnce)
}
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
if foreground {
return serveOffers(offers, pasteOnce)
buf, err := io.ReadAll(data)
if err != nil {
return fmt.Errorf("read source: %w", err)
}
return serveClipboard(buf, mimeType, pasteOnce)
}
return copyMultiFork(offers, pasteOnce)
return copyFork(data, mimeType, pasteOnce)
}
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
@@ -173,70 +132,39 @@ func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
cmd := newForkCmd(mimeType, pasteOnce)
if src, ok := data.(*os.File); ok {
switch src := data.(type) {
case *os.File:
cmd.Stdin = src
return waitReady(cmd)
}
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("stdin pipe: %w", err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("stdout pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
if _, err := io.Copy(stdin, data); err != nil {
stdin.Close()
return fmt.Errorf("write stdin: %w", err)
}
if err := stdin.Close(); err != nil {
return fmt.Errorf("close stdin: %w", err)
}
var buf [1]byte
if _, err := stdout.Read(buf[:]); err != nil {
return fmt.Errorf("waiting for clipboard ready: %w", err)
}
return nil
}
func copyMultiFork(offers []Offer, pasteOnce bool) error {
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
if pasteOnce {
args = append(args, "--paste-once")
}
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("stdin pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
for _, offer := range offers {
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
if _, err := stdin.Write(offer.Data); err != nil {
stdin.Close()
return fmt.Errorf("write offer data: %w", err)
default:
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("stdin pipe: %w", err)
}
}
stdin.Close()
return nil
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("stdout pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
if _, err := io.Copy(stdin, data); err != nil {
stdin.Close()
return fmt.Errorf("write stdin: %w", err)
}
if err := stdin.Close(); err != nil {
return fmt.Errorf("close stdin: %w", err)
}
var buf [1]byte
if _, err := stdout.Read(buf[:]); err != nil {
return fmt.Errorf("waiting for clipboard ready: %w", err)
}
return nil
}
}
func signalReady() {
@@ -266,25 +194,57 @@ func createClipboardCacheFile() (*os.File, error) {
return os.CreateTemp("", "dms-clipboard-*")
}
// serveOffers owns the Wayland selection until cancelled (or first paste when
// pasteOnce is set), answering every offered mime type with its data.
func serveOffers(offers []Offer, pasteOnce bool) error {
if len(offers) == 0 {
return fmt.Errorf("no offers to serve")
}
s, err := connectSession()
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
display, err := wlclient.Connect("")
if err != nil {
return err
return fmt.Errorf("wayland connect: %w", err)
}
defer s.Close()
defer display.Destroy()
dataControlMgr, err := s.requireDataControl()
ctx := display.Context()
registry, err := display.GetRegistry()
if err != nil {
return err
return fmt.Errorf("get registry: %w", err)
}
defer registry.Destroy()
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
var seat *wlclient.Seat
var bindErr error
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case "ext_data_control_manager_v1":
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
bindErr = err
}
case "wl_seat":
if seat != nil {
return
}
seat = wlclient.NewSeat(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
bindErr = err
}
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
return fmt.Errorf("registry bind: %w", bindErr)
}
if dataControlMgr == nil {
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return fmt.Errorf("no seat available")
}
device, err := dataControlMgr.GetDataDevice(s.seat)
device, err := dataControlMgr.GetDataDevice(seat)
if err != nil {
return fmt.Errorf("get data device: %w", err)
}
@@ -295,12 +255,25 @@ func serveOffers(offers []Offer, pasteOnce bool) error {
return fmt.Errorf("create data source: %w", err)
}
offerData := make(map[string][]byte, len(offers))
for _, offer := range offers {
if err := source.Offer(offer.MimeType); err != nil {
return fmt.Errorf("offer %s: %w", offer.MimeType, err)
if err := source.Offer(mimeType); err != nil {
return fmt.Errorf("offer mime type: %w", err)
}
if mimeType == "text/plain;charset=utf-8" || mimeType == "text/plain" {
if err := source.Offer("text/plain"); err != nil {
return fmt.Errorf("offer text/plain: %w", err)
}
if err := source.Offer("text/plain;charset=utf-8"); err != nil {
return fmt.Errorf("offer text/plain;charset=utf-8: %w", err)
}
if err := source.Offer("UTF8_STRING"); err != nil {
return fmt.Errorf("offer UTF8_STRING: %w", err)
}
if err := source.Offer("STRING"); err != nil {
return fmt.Errorf("offer STRING: %w", err)
}
if err := source.Offer("TEXT"); err != nil {
return fmt.Errorf("offer TEXT: %w", err)
}
offerData[offer.MimeType] = offer.Data
}
cancelled := make(chan struct{})
@@ -310,11 +283,7 @@ func serveOffers(offers []Offer, pasteOnce bool) error {
_ = syscall.SetNonblock(e.Fd, false)
file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close()
if data, ok := offerData[e.MimeType]; ok {
_, _ = file.Write(data)
}
_, _ = file.Write(data)
select {
case pasted <- struct{}{}:
default:
@@ -329,7 +298,7 @@ func serveOffers(offers []Offer, pasteOnce bool) error {
return fmt.Errorf("set selection: %w", err)
}
s.display.Roundtrip()
display.Roundtrip()
signalReady()
for {
@@ -341,26 +310,70 @@ func serveOffers(offers []Offer, pasteOnce bool) error {
return nil
}
default:
if err := s.ctx.Dispatch(); err != nil {
if err := ctx.Dispatch(); err != nil {
return nil
}
}
}
}
func CopyText(text string) error {
return Copy([]byte(text), "text/plain;charset=utf-8")
}
func Paste() ([]byte, string, error) {
s, err := connectSession()
display, err := wlclient.Connect("")
if err != nil {
return nil, "", err
return nil, "", fmt.Errorf("wayland connect: %w", err)
}
defer s.Close()
defer display.Destroy()
dataControlMgr, err := s.requireDataControl()
ctx := display.Context()
registry, err := display.GetRegistry()
if err != nil {
return nil, "", err
return nil, "", fmt.Errorf("get registry: %w", err)
}
defer registry.Destroy()
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
var seat *wlclient.Seat
var bindErr error
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case "ext_data_control_manager_v1":
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
bindErr = err
}
case "wl_seat":
if seat != nil {
return
}
seat = wlclient.NewSeat(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
bindErr = err
}
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
return nil, "", fmt.Errorf("registry bind: %w", bindErr)
}
device, err := dataControlMgr.GetDataDevice(s.seat)
if dataControlMgr == nil {
return nil, "", fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return nil, "", fmt.Errorf("no seat available")
}
device, err := dataControlMgr.GetDataDevice(seat)
if err != nil {
return nil, "", fmt.Errorf("get data device: %w", err)
}
@@ -386,14 +399,15 @@ func Paste() ([]byte, string, error) {
gotSelection = true
})
s.display.Roundtrip()
s.display.Roundtrip()
display.Roundtrip()
display.Roundtrip()
if !gotSelection || selectionOffer == nil {
return nil, "", fmt.Errorf("no clipboard data")
}
selectedMime := selectPreferredMimeType(offerMimeTypes[selectionOffer])
mimeTypes := offerMimeTypes[selectionOffer]
selectedMime := selectPreferredMimeType(mimeTypes)
if selectedMime == "" {
return nil, "", fmt.Errorf("no supported mime type")
}
@@ -410,7 +424,7 @@ func Paste() ([]byte, string, error) {
}
w.Close()
s.display.Roundtrip()
display.Roundtrip()
data, err := io.ReadAll(r)
if err != nil {
@@ -456,3 +470,161 @@ func selectPreferredMimeType(mimes []string) string {
func IsImageMimeType(mime string) bool {
return len(mime) > 6 && mime[:6] == "image/"
}
type Offer struct {
MimeType string
Data []byte
}
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
if !foreground {
return copyMultiFork(offers, pasteOnce)
}
return copyMultiServe(offers, pasteOnce)
}
func copyMultiFork(offers []Offer, pasteOnce bool) error {
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
if pasteOnce {
args = append(args, "--paste-once")
}
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("stdin pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
for _, offer := range offers {
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
if _, err := stdin.Write(offer.Data); err != nil {
stdin.Close()
return fmt.Errorf("write offer data: %w", err)
}
}
stdin.Close()
return nil
}
func copyMultiServe(offers []Offer, pasteOnce bool) error {
display, err := wlclient.Connect("")
if err != nil {
return fmt.Errorf("wayland connect: %w", err)
}
defer display.Destroy()
ctx := display.Context()
registry, err := display.GetRegistry()
if err != nil {
return fmt.Errorf("get registry: %w", err)
}
defer registry.Destroy()
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
var seat *wlclient.Seat
var bindErr error
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case "ext_data_control_manager_v1":
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
bindErr = err
}
case "wl_seat":
if seat != nil {
return
}
seat = wlclient.NewSeat(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
bindErr = err
}
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
return fmt.Errorf("registry bind: %w", bindErr)
}
if dataControlMgr == nil {
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return fmt.Errorf("no seat available")
}
device, err := dataControlMgr.GetDataDevice(seat)
if err != nil {
return fmt.Errorf("get data device: %w", err)
}
defer device.Destroy()
source, err := dataControlMgr.CreateDataSource()
if err != nil {
return fmt.Errorf("create data source: %w", err)
}
offerMap := make(map[string][]byte)
for _, offer := range offers {
if err := source.Offer(offer.MimeType); err != nil {
return fmt.Errorf("offer %s: %w", offer.MimeType, err)
}
offerMap[offer.MimeType] = offer.Data
}
cancelled := make(chan struct{})
pasted := make(chan struct{}, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
_ = syscall.SetNonblock(e.Fd, false)
file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close()
if data, ok := offerMap[e.MimeType]; ok {
_, _ = file.Write(data)
}
select {
case pasted <- struct{}{}:
default:
}
})
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
close(cancelled)
})
if err := device.SetSelection(source); err != nil {
return fmt.Errorf("set selection: %w", err)
}
display.Roundtrip()
for {
select {
case <-cancelled:
return nil
case <-pasted:
if pasteOnce {
return nil
}
default:
if err := ctx.Dispatch(); err != nil {
return nil
}
}
}
}
-206
View File
@@ -1,206 +0,0 @@
package clipboard
import (
"fmt"
"regexp"
"strconv"
"strings"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"golang.org/x/sys/unix"
)
const (
xkbKeymapFormatV1 = 1
keyStateReleased = 0
keyStatePressed = 1
// xkb real modifier bit positions are fixed: Shift=0, Lock=1, Control=2
shiftModMask = 1 << 0
ctrlModMask = 1 << 2
// evdev fallbacks for a standard pc105 map
fallbackCtrlKey = 29 // KEY_LEFTCTRL
fallbackShiftKey = 42 // KEY_LEFTSHIFT
fallbackVKey = 47 // KEY_V
)
// SendPasteKeystroke emulates a paste shortcut via zwp_virtual_keyboard_v1
// using the seat's own keymap, so keycodes stay valid for XWayland clients
// (a synthetic wtype-style keymap breaks X11 apps like Steam). withShift
// selects ctrl+shift+v for terminal targets.
func SendPasteKeystroke(withShift bool) error {
s, err := connectSession()
if err != nil {
return err
}
defer s.Close()
if s.virtualKeyboardMgr == nil {
return fmt.Errorf("compositor does not support zwp_virtual_keyboard_manager_v1")
}
if s.seat == nil {
return fmt.Errorf("no seat available")
}
keyboard, err := s.seat.GetKeyboard()
if err != nil {
return fmt.Errorf("get keyboard: %w", err)
}
defer keyboard.Release()
var keymap *wlclient.KeyboardKeymapEvent
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
if keymap == nil {
keymap = &e
}
})
s.display.Roundtrip()
if keymap == nil || keymap.Format != xkbKeymapFormatV1 {
return fmt.Errorf("no xkb keymap from seat")
}
defer unix.Close(keymap.Fd)
keymapText, err := readKeymap(keymap.Fd, keymap.Size)
if err != nil {
return fmt.Errorf("read keymap: %w", err)
}
keys := resolveKeycodes(keymapText)
vk, err := s.virtualKeyboardMgr.CreateVirtualKeyboard(s.seat)
if err != nil {
return fmt.Errorf("create virtual keyboard: %w", err)
}
defer vk.Destroy()
if err := vk.Keymap(xkbKeymapFormatV1, keymap.Fd, keymap.Size); err != nil {
return fmt.Errorf("set keymap: %w", err)
}
mods := uint32(ctrlModMask)
held := []uint32{keys.ctrl}
if withShift {
mods |= shiftModMask
held = append(held, keys.shift)
}
t := uint32(0)
press := func(key, state uint32) error {
t++
return vk.Key(t, key, state)
}
for _, key := range held {
if err := press(key, keyStatePressed); err != nil {
return fmt.Errorf("key press: %w", err)
}
}
if err := vk.Modifiers(mods, 0, 0, 0); err != nil {
return fmt.Errorf("set modifiers: %w", err)
}
if err := press(keys.v, keyStatePressed); err != nil {
return fmt.Errorf("key press: %w", err)
}
if err := press(keys.v, keyStateReleased); err != nil {
return fmt.Errorf("key release: %w", err)
}
for i := len(held) - 1; i >= 0; i-- {
if err := press(held[i], keyStateReleased); err != nil {
return fmt.Errorf("key release: %w", err)
}
}
if err := vk.Modifiers(0, 0, 0, 0); err != nil {
return fmt.Errorf("clear modifiers: %w", err)
}
s.display.Roundtrip()
return nil
}
func readKeymap(fd int, size uint32) (string, error) {
data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ, unix.MAP_PRIVATE)
if err != nil {
return "", err
}
text := strings.TrimRight(string(data), "\x00")
return text, unix.Munmap(data)
}
type pasteKeycodes struct {
ctrl uint32
shift uint32
v uint32
}
var (
keycodeDefRe = regexp.MustCompile(`<([A-Za-z0-9+_-]+)>\s*=\s*(\d+)`)
keySymbolsRe = regexp.MustCompile(`key\s*<([A-Za-z0-9+_-]+)>\s*\{([^}]*)\}`)
groupIndexRe = regexp.MustCompile(`\w+\[\d+\]\s*=`)
symbolListRe = regexp.MustCompile(`\[([^\]]*)\]`)
)
// xkbcommon may serialize keysyms as hex escapes instead of names
// (e.g. "0x76" for v, "0xffe3" for Control_L).
var keysymNames = map[uint32]string{
0x76: "v",
0xffe3: "Control_L",
0xffe1: "Shift_L",
}
func canonicalKeysym(sym string) string {
if !strings.HasPrefix(sym, "0x") && !strings.HasPrefix(sym, "0X") {
return sym
}
value, err := strconv.ParseUint(sym[2:], 16, 32)
if err != nil {
return sym
}
if name, ok := keysymNames[uint32(value)]; ok {
return name
}
return sym
}
// resolveKeycodes finds the evdev keycodes producing the keysyms we need in
// the seat keymap's first group, falling back to pc105 positions.
func resolveKeycodes(keymap string) pasteKeycodes {
codes := map[string]uint32{}
for _, m := range keycodeDefRe.FindAllStringSubmatch(keymap, -1) {
if code, err := strconv.Atoi(m[2]); err == nil {
codes[m[1]] = uint32(code)
}
}
keys := pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}
want := map[string]*uint32{
"Control_L": &keys.ctrl,
"Shift_L": &keys.shift,
"v": &keys.v,
}
for _, m := range keySymbolsRe.FindAllStringSubmatch(keymap, -1) {
group := symbolListRe.FindStringSubmatch(groupIndexRe.ReplaceAllString(m[2], ""))
if group == nil {
continue
}
xkbCode, ok := codes[m[1]]
if !ok || xkbCode < 8 {
continue
}
level1 := canonicalKeysym(strings.TrimSpace(strings.Split(group[1], ",")[0]))
target, wanted := want[level1]
if !wanted {
continue
}
*target = xkbCode - 8
delete(want, level1)
if len(want) == 0 {
break
}
}
return keys
}
@@ -1,65 +0,0 @@
package clipboard
import (
"os"
"testing"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"golang.org/x/sys/unix"
)
func TestLiveSeatKeymapResolution(t *testing.T) {
if os.Getenv("DMS_LIVE_TEST") == "" {
t.Skip("set DMS_LIVE_TEST=1 to run against the live compositor")
}
s, err := connectSession()
if err != nil {
t.Fatalf("connect: %v", err)
}
defer s.Close()
if s.virtualKeyboardMgr == nil {
t.Fatal("compositor does not advertise zwp_virtual_keyboard_manager_v1")
}
if s.seat == nil {
t.Fatal("no seat")
}
keyboard, err := s.seat.GetKeyboard()
if err != nil {
t.Fatalf("get keyboard: %v", err)
}
defer keyboard.Release()
var keymap *wlclient.KeyboardKeymapEvent
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
if keymap == nil {
keymap = &e
}
})
s.display.Roundtrip()
if keymap == nil {
t.Fatal("no keymap event")
}
defer unix.Close(keymap.Fd)
text, err := readKeymap(keymap.Fd, keymap.Size)
if err != nil {
t.Fatalf("read keymap: %v", err)
}
if dump := os.Getenv("DMS_LIVE_DUMP"); dump != "" {
if err := os.WriteFile(dump, []byte(text), 0o644); err != nil {
t.Fatalf("dump keymap: %v", err)
}
}
keys := resolveKeycodes(text)
t.Logf("keymap size=%d resolved ctrl=%d shift=%d v=%d", keymap.Size, keys.ctrl, keys.shift, keys.v)
if keys.ctrl == fallbackCtrlKey && keys.shift == fallbackShiftKey && keys.v == fallbackVKey {
t.Log("all keycodes are fallbacks - parsing may not have matched the live keymap")
}
}
-105
View File
@@ -1,105 +0,0 @@
package clipboard
import "testing"
const qwertyKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
minimum = 8;
maximum = 708;
<ESC> = 9;
<AB04> = 55;
<LCTL> = 37;
<LFSH> = 50;
alias <AL01> = <AC01>;
indicator 1 = "Caps Lock";
};
xkb_types "(unnamed)" {
type "ALPHABETIC" {
modifiers = Shift+Lock;
map[Shift] = Level2;
level_name[Level1] = "Base";
};
};
xkb_symbols "(unnamed)" {
key <ESC> { [ Escape ] };
key <AB04> { type= "ALPHABETIC", [ v, V ] };
key <LCTL> { [ Control_L ] };
key <LFSH> { [ Shift_L ] };
};
};`
const hexKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
<AB04> = 56;
<LCTL> = 38;
<LFSH> = 51;
};
xkb_symbols "(unnamed)" {
key <LCTL> { [ 0xffe3 ] };
key <LFSH> {
type= "PC_ALT_LEVEL2",
symbols[1]= [ 0xffe1, 0xfe08 ]
};
key <AB04> { [ 0x76, 0x56 ] };
};
};`
const dvorakKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
<AB09> = 60;
<LCTL> = 37;
<LFSH> = 50;
};
xkb_symbols "(unnamed)" {
key <AB09> { [ v, V ] };
key <LCTL> { [ Control_L ] };
key <LFSH> { [ Shift_L ] };
};
};`
func TestResolveKeycodes(t *testing.T) {
tests := []struct {
name string
keymap string
want pasteKeycodes
}{
{"qwerty", qwertyKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 47}},
{"dvorak", dvorakKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 52}},
{"hex keysyms", hexKeymap, pasteKeycodes{ctrl: 30, shift: 43, v: 48}},
{"empty falls back", "", pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := resolveKeycodes(tt.keymap)
if got != tt.want {
t.Errorf("resolveKeycodes() = %+v, want %+v", got, tt.want)
}
})
}
}
func TestExpandOffers(t *testing.T) {
text := ExpandOffers([]byte("hi"), "text/plain;charset=utf-8")
if len(text) != 5 {
t.Fatalf("expected 5 text offers, got %d", len(text))
}
seen := map[string]bool{}
for _, o := range text {
if string(o.Data) != "hi" {
t.Errorf("offer %s has wrong data", o.MimeType)
}
if seen[o.MimeType] {
t.Errorf("duplicate offer %s", o.MimeType)
}
seen[o.MimeType] = true
}
if !seen["UTF8_STRING"] || !seen["STRING"] || !seen["TEXT"] || !seen["text/plain"] {
t.Errorf("missing X11 alias offers: %v", seen)
}
img := ExpandOffers([]byte{1}, "image/png")
if len(img) != 1 || img[0].MimeType != "image/png" {
t.Errorf("non-text mime should not expand, got %+v", img)
}
}
-93
View File
@@ -1,93 +0,0 @@
package clipboard
import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/virtual_keyboard"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type session struct {
display *wlclient.Display
ctx *wlclient.Context
registry *wlclient.Registry
seat *wlclient.Seat
dataControlMgr *ext_data_control.ExtDataControlManagerV1
virtualKeyboardMgr *virtual_keyboard.ZwpVirtualKeyboardManagerV1
}
// connectSession opens a short-lived Wayland connection and binds the seat
// plus whichever clipboard-related globals the compositor advertises.
func connectSession() (*session, error) {
display, err := wlclient.Connect("")
if err != nil {
return nil, fmt.Errorf("wayland connect: %w", err)
}
s := &session{display: display, ctx: display.Context()}
registry, err := display.GetRegistry()
if err != nil {
display.Destroy()
return nil, fmt.Errorf("get registry: %w", err)
}
s.registry = registry
var bindErr error
bind := func(name uint32, iface string, version uint32, proxy wlclient.Proxy) {
if err := registry.Bind(name, iface, version, proxy); err != nil {
bindErr = fmt.Errorf("bind %s: %w", iface, err)
}
}
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case ext_data_control.ExtDataControlManagerV1InterfaceName:
mgr := ext_data_control.NewExtDataControlManagerV1(s.ctx)
bind(e.Name, e.Interface, e.Version, mgr)
s.dataControlMgr = mgr
case virtual_keyboard.ZwpVirtualKeyboardManagerV1InterfaceName:
mgr := virtual_keyboard.NewZwpVirtualKeyboardManagerV1(s.ctx)
bind(e.Name, e.Interface, e.Version, mgr)
s.virtualKeyboardMgr = mgr
case "wl_seat":
if s.seat != nil {
return
}
seat := wlclient.NewSeat(s.ctx)
bind(e.Name, e.Interface, e.Version, seat)
s.seat = seat
}
})
display.Roundtrip()
display.Roundtrip()
if bindErr != nil {
s.Close()
return nil, bindErr
}
return s, nil
}
func (s *session) requireDataControl() (*ext_data_control.ExtDataControlManagerV1, error) {
switch {
case s.dataControlMgr == nil:
return nil, fmt.Errorf("compositor does not support ext_data_control_manager_v1")
case s.seat == nil:
return nil, fmt.Errorf("no seat available")
default:
return s.dataControlMgr, nil
}
}
func (s *session) Close() {
if s.dataControlMgr != nil {
s.dataControlMgr.Destroy()
}
if s.registry != nil {
s.registry.Destroy()
}
s.display.Destroy()
}
-4
View File
@@ -61,10 +61,6 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
}
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstallsAndSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, useSystemd)
}
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
var results []DeploymentResult
+2 -2
View File
@@ -20,8 +20,8 @@ mouse-hide-while-typing = true
copy-on-select = false
confirm-close-surface = false
# Disable in-app Ghostty toast notifications
app-notifications = false
# Disable annoying copied to clipboard
app-notifications = no-clipboard-copy,no-config-reload
# Key bindings for common actions
#keybind = ctrl+c=copy_to_clipboard
+1 -1
View File
@@ -9,7 +9,7 @@ hl.bind("SUPER + M", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle"))
hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle"))
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle"))
hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dash toggle wallpaper"))
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dankdash wallpaper"))
hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
hl.bind("SUPER + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
@@ -22,7 +22,7 @@ bind=SUPER,n,spawn,dms ipc call notifications toggle
# Notepad
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
# Browse Wallpapers
bind=SUPER,y,spawn,dms ipc call dash toggle wallpaper
bind=SUPER,y,spawn,dms ipc call dankdash wallpaper
# Power Menu
bind=SUPER,x,spawn,dms ipc call powermenu toggle
# Cycle Display Profile
+1 -1
View File
@@ -24,7 +24,7 @@ binds {
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
}
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
spawn "dms" "ipc" "call" "dash" "toggle" "wallpaper";
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
}
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
@@ -20,10 +20,3 @@ window-rule {
tiled-state true
draw-border-with-background false
}
layer-rule {
exclude namespace="^dms:bar$"
background-effect {
xray false
}
}
+6 -6
View File
@@ -265,9 +265,9 @@ recent-windows {
}
// Include dms files
include optional=true "dms/colors.kdl"
include optional=true "dms/layout.kdl"
include optional=true "dms/alttab.kdl"
include optional=true "dms/binds.kdl"
include optional=true "dms/outputs.kdl"
include optional=true "dms/cursor.kdl"
include "dms/colors.kdl"
include "dms/layout.kdl"
include "dms/alttab.kdl"
include "dms/binds.kdl"
include "dms/outputs.kdl"
include "dms/cursor.kdl"
-3
View File
@@ -17,7 +17,6 @@ const (
FamilyDebian DistroFamily = "debian"
FamilyNix DistroFamily = "nix"
FamilyGentoo DistroFamily = "gentoo"
FamilyVoid DistroFamily = "void"
)
// PackageManagerType defines the package manager a distro uses
@@ -30,7 +29,6 @@ const (
PackageManagerZypper PackageManagerType = "zypper"
PackageManagerNix PackageManagerType = "nix"
PackageManagerPortage PackageManagerType = "portage"
PackageManagerXBPS PackageManagerType = "xbps"
)
// RepositoryType defines the type of repository for a package
@@ -44,7 +42,6 @@ const (
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
RepoTypeFlake RepositoryType = "flake" // Nix flake
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
RepoTypeXBPS RepositoryType = "xbps" // Custom XBPS repository
RepoTypeManual RepositoryType = "manual" // Manual build from source
)
-530
View File
@@ -1,530 +0,0 @@
package distros
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
)
const (
VoidDMSRepo = "https://avengemedia.github.io/DankMaterialShell/current"
VoidDankLinuxRepo = "https://avengemedia.github.io/DankLinux/current"
VoidHyprlandRepo = "https://mirror.black-hole.dev/x86_64"
voidRunitSvDir = "/etc/sv"
voidRunitServiceDir = "/var/service"
)
func init() {
Register("void", "#478061", FamilyVoid, func(config DistroConfig, logChan chan<- string) Distribution {
return NewVoidDistribution(config, logChan)
})
}
type VoidDistribution struct {
*BaseDistribution
config DistroConfig
}
func NewVoidDistribution(config DistroConfig, logChan chan<- string) *VoidDistribution {
return &VoidDistribution{
BaseDistribution: NewBaseDistribution(logChan),
config: config,
}
}
func (v *VoidDistribution) GetID() string {
return v.config.ID
}
func (v *VoidDistribution) GetColorHex() string {
return v.config.ColorHex
}
func (v *VoidDistribution) GetFamily() DistroFamily {
return v.config.Family
}
func (v *VoidDistribution) GetPackageManager() PackageManagerType {
return PackageManagerXBPS
}
func (v *VoidDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
return v.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
}
func (v *VoidDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
var dependencies []deps.Dependency
dependencies = append(dependencies, v.detectDMS())
dependencies = append(dependencies, v.detectSpecificTerminal(terminal))
dependencies = append(dependencies, v.detectGit())
dependencies = append(dependencies, v.detectWindowManager(wm))
dependencies = append(dependencies, v.detectQuickshell())
dependencies = append(dependencies, v.detectDMSGreeter())
dependencies = append(dependencies, v.detectXDGPortal())
dependencies = append(dependencies, v.detectAccountsService())
dependencies = append(dependencies, v.detectDBus())
dependencies = append(dependencies, v.detectElogind())
if wm == deps.WindowManagerHyprland {
dependencies = append(dependencies, v.detectHyprlandTools()...)
}
if wm == deps.WindowManagerNiri || wm == deps.WindowManagerMango {
dependencies = append(dependencies, v.detectXwaylandSatellite())
}
dependencies = append(dependencies, v.detectMatugen())
dependencies = append(dependencies, v.detectDgop())
return dependencies, nil
}
func (v *VoidDistribution) detectDMS() deps.Dependency {
status := deps.StatusMissing
version := ""
variant := deps.VariantStable
if v.packageInstalled("dms-git") {
status = deps.StatusInstalled
version = v.packageVersion("dms-git")
variant = deps.VariantGit
} else if v.packageInstalled("dms") {
status = deps.StatusInstalled
version = v.packageVersion("dms")
} else if v.commandExists("dms") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "dms (DankMaterialShell)",
Status: status,
Version: version,
Description: "Desktop Management System package",
Required: true,
Variant: variant,
CanToggle: true,
}
}
func (v *VoidDistribution) detectQuickshell() deps.Dependency {
dep := v.BaseDistribution.detectQuickshell()
dep.CanToggle = false
return dep
}
func (v *VoidDistribution) detectXDGPortal() deps.Dependency {
return v.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", v.packageInstalled("xdg-desktop-portal-gtk"))
}
func (v *VoidDistribution) detectDMSGreeter() deps.Dependency {
return v.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", v.packageInstalled("dms-greeter"))
}
func (v *VoidDistribution) detectAccountsService() deps.Dependency {
return v.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", v.packageInstalled("accountsservice"))
}
func (v *VoidDistribution) detectDBus() deps.Dependency {
return v.detectPackage("dbus", "D-Bus system and session message bus", v.packageInstalled("dbus"))
}
func (v *VoidDistribution) detectElogind() deps.Dependency {
return v.detectPackage("elogind", "loginctl/logind provider for power management and session tracking", v.packageInstalled("elogind") || v.commandExists("loginctl"))
}
func (v *VoidDistribution) detectXwaylandSatellite() deps.Dependency {
return v.detectPackage("xwayland-satellite", "Xwayland support", v.packageInstalled("xwayland-satellite"))
}
func (v *VoidDistribution) packageInstalled(pkg string) bool {
return exec.Command("xbps-query", pkg).Run() == nil
}
func (v *VoidDistribution) packageVersion(pkg string) string {
output, err := exec.Command("xbps-query", "-p", "pkgver", pkg).Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(output))
}
func (v *VoidDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return v.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
func (v *VoidDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
packages := map[string]PackageMapping{
"git": {Name: "git", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"dbus": {Name: "dbus", Repository: RepoTypeSystem},
"elogind": {Name: "elogind", Repository: RepoTypeSystem},
"quickshell": {Name: "quickshell", Repository: RepoTypeSystem},
"matugen": {Name: "matugen", Repository: RepoTypeSystem},
"dms (DankMaterialShell)": v.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo},
"dgop": {Name: "dgop", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
}
switch wm {
case deps.WindowManagerHyprland:
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo}
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem}
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
case deps.WindowManagerMango:
packages["mango"] = PackageMapping{Name: "mangowc", Repository: RepoTypeSystem}
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
}
return packages
}
func (v *VoidDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantStable {
return PackageMapping{Name: "dms", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo}
}
return PackageMapping{Name: "dms-git", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo}
}
func (v *VoidDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.06,
Step: "Checking XBPS...",
IsComplete: false,
LogOutput: "Checking for xbps-install",
}
if _, err := exec.LookPath("xbps-install"); err != nil {
return fmt.Errorf("xbps-install not found; Void Linux package tools are required: %w", err)
}
return nil
}
func (v *VoidDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.05,
Step: "Checking system prerequisites...",
IsComplete: false,
LogOutput: "Starting prerequisite check...",
}
if wm == deps.WindowManagerHyprland {
arch, err := v.xbpsArch(ctx)
if err != nil {
return fmt.Errorf("failed to detect XBPS architecture for Hyprland repository selection: %w", err)
}
if arch != "x86_64" {
return fmt.Errorf("hyprland on Void Linux is installed from %s, which currently provides x86_64 packages only (detected %s)", VoidHyprlandRepo, arch)
}
}
if err := v.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
systemPkgs, xbpsPkgs := v.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
if len(xbpsPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.15,
Step: "Enabling DMS XBPS repositories...",
IsComplete: false,
NeedsSudo: true,
LogOutput: "Setting up custom XBPS repositories for DMS packages",
}
if err := v.enableXBPSRepos(ctx, xbpsPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable XBPS repositories: %w", err)
}
}
allPkgs := v.uniquePackageNames(systemPkgs, v.extractPackageNames(xbpsPkgs))
if len(allPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: fmt.Sprintf("Installing %d XBPS packages...", len(allPkgs)),
IsComplete: false,
NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing XBPS packages: %s", strings.Join(allPkgs, ", ")),
}
if err := v.installXBPSPackages(ctx, allPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install XBPS packages: %w", err)
}
}
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.90,
Step: "Configuring system...",
IsComplete: false,
LogOutput: "Starting post-installation configuration...",
}
v.log("Void Linux detected; DMS environment and autostart will be configured in the compositor config instead of systemd")
if err := v.ensureSessionServices(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable Void session services: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
Progress: 1.0,
Step: "Installation complete!",
IsComplete: true,
LogOutput: "All packages installed and configured successfully",
}
return nil
}
func (v *VoidDistribution) ensureSessionServices(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if !v.isRunitSystem() {
v.log("Void runit service directory not detected; skipping dbus/elogind service enablement")
return nil
}
for _, service := range []string{"dbus", "elogind"} {
if !v.runitServiceInstalled(service) {
v.log(fmt.Sprintf("Warning: %s runit service not found in %s; power/session actions may not work until %s is installed", service, voidRunitSvDir, service))
continue
}
if v.runitServiceEnabled(service) {
v.log(fmt.Sprintf("Void runit service %s already enabled", service))
continue
}
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.92,
Step: fmt.Sprintf("Enabling %s runit service...", service),
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service)),
LogOutput: fmt.Sprintf("Enabling Void runit service: %s", service),
}
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service)))
if err := v.runWithProgress(cmd, progressChan, PhaseConfiguration, 0.92, 0.95); err != nil {
return fmt.Errorf("failed to enable %s runit service: %w", service, err)
}
v.log(fmt.Sprintf("✓ Enabled %s runit service", service))
}
return nil
}
func (v *VoidDistribution) isRunitSystem() bool {
if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() {
return true
}
if _, err := os.Stat("/run/systemd/system"); err == nil {
return false
}
if fi, err := os.Stat(voidRunitServiceDir); err == nil && fi.IsDir() {
return true
}
return false
}
func (v *VoidDistribution) runitServiceInstalled(name string) bool {
fi, err := os.Stat(filepath.Join(voidRunitSvDir, name))
return err == nil && fi.IsDir()
}
func (v *VoidDistribution) runitServiceEnabled(name string) bool {
_, err := os.Lstat(filepath.Join(voidRunitServiceDir, name))
return err == nil
}
func (v *VoidDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping) {
systemPkgs := []string{}
xbpsPkgs := []PackageMapping{}
variantMap := make(map[string]deps.PackageVariant)
for _, dep := range dependencies {
variantMap[dep.Name] = dep.Variant
}
packageMap := v.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
continue
}
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
continue
}
pkgInfo, exists := packageMap[dep.Name]
if !exists {
v.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
continue
}
switch pkgInfo.Repository {
case RepoTypeXBPS:
xbpsPkgs = append(xbpsPkgs, pkgInfo)
case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name)
}
}
return systemPkgs, xbpsPkgs
}
func (v *VoidDistribution) enableXBPSRepos(ctx context.Context, xbpsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
enabledRepoURLs := []string{}
for _, pkg := range xbpsPkgs {
if pkg.RepoURL == "" || enabledRepos[pkg.RepoURL] {
continue
}
repoName := v.xbpsRepoName(pkg.RepoURL)
confPath := filepath.Join("/etc/xbps.d", repoName+".conf")
repoLine := fmt.Sprintf("repository=%s", pkg.RepoURL)
repoFileContent := repoLine + "\n"
if content, err := os.ReadFile(confPath); err == nil && string(content) == repoFileContent {
v.log(fmt.Sprintf("XBPS repo %s already configured, skipping", pkg.RepoURL))
enabledRepos[pkg.RepoURL] = true
enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL)
continue
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.18,
Step: fmt.Sprintf("Adding XBPS repo %s...", repoName),
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("echo 'repository=%s' | sudo tee %s", pkg.RepoURL, confPath),
LogOutput: fmt.Sprintf("Adding XBPS repository: %s", pkg.RepoURL),
}
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/xbps.d")
if err := v.runWithProgress(mkdirCmd, progressChan, PhaseSystemPackages, 0.18, 0.19); err != nil {
return fmt.Errorf("failed to create /etc/xbps.d: %w", err)
}
writeCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c 'printf \"%%s\\n\" %q > %s'", repoLine, confPath))
if err := v.runWithProgress(writeCmd, progressChan, PhaseSystemPackages, 0.19, 0.22); err != nil {
return fmt.Errorf("failed to add XBPS repo %s: %w", pkg.RepoURL, err)
}
enabledRepos[pkg.RepoURL] = true
enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL)
}
if len(enabledRepos) > 0 {
syncArgs := []string{"xbps-install", "-Sy", "-i"}
for _, repoURL := range enabledRepoURLs {
syncArgs = append(syncArgs, "--repository", repoURL)
}
syncCommand := strings.Join(syncArgs, " ")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.25,
Step: "Synchronizing XBPS repositories...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo sh -c 'yes y | " + syncCommand + "'",
LogOutput: "Synchronizing XBPS repository indexes",
}
syncCmd := privesc.ExecCommand(ctx, sudoPassword, "sh -c 'yes y | "+syncCommand+"'")
if err := v.runWithProgress(syncCmd, progressChan, PhaseSystemPackages, 0.25, 0.30); err != nil {
return fmt.Errorf("failed to synchronize XBPS repositories: %w", err)
}
}
return nil
}
func (v *VoidDistribution) xbpsRepoName(repoURL string) string {
switch repoURL {
case VoidDMSRepo:
return "dms"
case VoidDankLinuxRepo:
return "danklinux"
case VoidHyprlandRepo:
return "hyprland"
default:
name := strings.TrimPrefix(repoURL, "https://")
name = strings.TrimPrefix(name, "http://")
name = strings.NewReplacer("/", "-", ".", "-").Replace(name)
return name
}
}
func (v *VoidDistribution) xbpsArch(ctx context.Context) (string, error) {
output, err := exec.CommandContext(ctx, "xbps-uhelper", "arch").Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}
func (v *VoidDistribution) installXBPSPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
args := append([]string{"xbps-install", "-Sy"}, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing XBPS packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return v.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.85)
}
func (v *VoidDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
names[i] = pkg.Name
}
return names
}
func (v *VoidDistribution) uniquePackageNames(groups ...[]string) []string {
seen := make(map[string]bool)
var unique []string
for _, group := range groups {
for _, pkg := range group {
if pkg == "" || seen[pkg] {
continue
}
seen[pkg] = true
unique = append(unique, pkg)
}
}
return unique
}
-150
View File
@@ -30,11 +30,6 @@ const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter"
const GreeterCacheDir = "/var/cache/dms-greeter"
const (
runitSvDir = "/etc/sv"
runitServiceDir = "/var/service"
)
func DetectDMSPath() (string, error) {
return config.LocateDMSConfig()
}
@@ -46,96 +41,6 @@ func IsNixOS() bool {
return err == nil
}
func IsVoidLinux() bool {
osInfo, err := distros.GetOSInfo()
if err != nil {
return false
}
config, exists := distros.Registry[osInfo.Distribution.ID]
return exists && config.Family == distros.FamilyVoid
}
func isRunit() bool {
if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() {
return true
}
if _, err := os.Stat("/run/systemd/system"); err == nil {
return false
}
if fi, err := os.Stat(runitServiceDir); err == nil && fi.IsDir() {
return true
}
return false
}
func runitServiceInstalled(name string) bool {
fi, err := os.Stat(filepath.Join(runitSvDir, name))
return err == nil && fi.IsDir()
}
func runitServiceEnabled(name string) bool {
_, err := os.Lstat(filepath.Join(runitServiceDir, name))
return err == nil
}
func enableRunitService(name, sudoPassword string) error {
if !runitServiceInstalled(name) {
return fmt.Errorf("runit service %q not found in %s", name, runitSvDir)
}
if runitServiceEnabled(name) {
return nil
}
return privesc.Run(context.Background(), sudoPassword, "ln", "-sf",
filepath.Join(runitSvDir, name), filepath.Join(runitServiceDir, name))
}
func disableRunitService(name, sudoPassword string) error {
if !runitServiceEnabled(name) {
return nil
}
return privesc.Run(context.Background(), sudoPassword, "rm", "-f",
filepath.Join(runitServiceDir, name))
}
func ensureRunitSeat(greeterUser, sudoPassword string, logFunc func(string)) {
if runitServiceInstalled("seatd") {
if err := enableRunitService("seatd", sudoPassword); err != nil {
logFunc(fmt.Sprintf("⚠ could not enable seatd: %v", err))
} else {
logFunc("✓ seatd enabled")
}
} else {
logFunc("⚠ seatd not installed — the greeter compositor needs it for GPU/seat access")
}
if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", "_seatd,video,input", greeterUser); err != nil {
logFunc(fmt.Sprintf("⚠ could not add %s to seat groups: %v", greeterUser, err))
} else {
logFunc(fmt.Sprintf("✓ %s added to seat groups (_seatd, video, input)", greeterUser))
}
}
func ensureGreetdPamRundir(sudoPassword string, logFunc func(string)) {
const pamPath = "/etc/pam.d/greetd"
data, err := os.ReadFile(pamPath)
if err != nil {
logFunc(fmt.Sprintf("⚠ could not read %s: %v", pamPath, err))
return
}
if strings.Contains(string(data), "pam_rundir") {
logFunc("✓ pam_rundir already present in greetd PAM")
return
}
line := "session optional pam_rundir.so"
if err := privesc.Run(context.Background(), sudoPassword, "sh", "-c",
fmt.Sprintf("printf '%%s\\n' %q >> %s", line, pamPath)); err != nil {
logFunc(fmt.Sprintf("⚠ could not add pam_rundir to %s: %v", pamPath, err))
return
}
logFunc("✓ pam_rundir added to greetd PAM (provides XDG_RUNTIME_DIR for the session)")
}
func DetectGreeterGroup() string {
data, err := os.ReadFile("/etc/group")
if err != nil {
@@ -861,8 +766,6 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd")
case distros.FamilyGentoo:
installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd")
case distros.FamilyVoid:
installCmd = privesc.ExecCommand(ctx, sudoPassword, "xbps-install -Sy greetd")
case distros.FamilyNix:
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
default:
@@ -989,14 +892,6 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
}
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Install from AUR: %s -S greetd-dms-greeter-git", aurHelper)
installCmd = exec.CommandContext(ctx, aurHelper, "-S", "--noconfirm", "greetd-dms-greeter-git")
case distros.FamilyVoid:
failHint = "⚠ dms-greeter install failed. Add the DMS XBPS repo manually:\necho 'repository=https://avengemedia.github.io/DankMaterialShell/current' | sudo tee /etc/xbps.d/dms.conf\nsudo xbps-install -Sy dms-greeter"
logFunc("Adding DMS XBPS repository...")
if err := ensureVoidXBPSRepo(ctx, sudoPassword, "dms", distros.VoidDMSRepo); err != nil {
logFunc(fmt.Sprintf("⚠ Failed to add DMS XBPS repository: %v", err))
}
privesc.ExecCommand(ctx, sudoPassword, "sh -c 'yes y | xbps-install -Sy -i --repository "+distros.VoidDMSRepo+"'").Run()
installCmd = privesc.ExecCommand(ctx, sudoPassword, "xbps-install -Sy dms-greeter")
default:
return false
}
@@ -1014,20 +909,6 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
return true
}
func ensureVoidXBPSRepo(ctx context.Context, sudoPassword, name, repoURL string) error {
confPath := filepath.Join("/etc/xbps.d", name+".conf")
repoLine := fmt.Sprintf("repository=%s", repoURL)
repoFileContent := repoLine + "\n"
if content, err := os.ReadFile(confPath); err == nil && string(content) == repoFileContent {
return nil
}
if err := privesc.Run(ctx, sudoPassword, "mkdir", "-p", "/etc/xbps.d"); err != nil {
return err
}
return privesc.Run(ctx, sudoPassword, "sh", "-c",
fmt.Sprintf("printf '%%s\\n' %q > %s", repoLine, confPath))
}
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
if IsGreeterPackaged() {
@@ -2394,19 +2275,6 @@ func checkSystemdEnabled(service string) (string, error) {
func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error {
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
for _, dm := range conflictingDMs {
if isRunit() {
if !runitServiceEnabled(dm) {
continue
}
logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm))
if err := disableRunitService(dm, sudoPassword); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err))
} else {
logFunc(fmt.Sprintf("✓ Disabled %s", dm))
}
continue
}
state, err := checkSystemdEnabled(dm)
if err != nil || state == "" || state == "not-found" {
continue
@@ -2426,19 +2294,6 @@ func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)
// EnableGreetd unmasks and enables greetd, forcing it over any other DM.
func EnableGreetd(sudoPassword string, logFunc func(string)) error {
if isRunit() {
if !runitServiceInstalled("greetd") {
return fmt.Errorf("greetd service not found in %s; ensure greetd is installed", runitSvDir)
}
ensureRunitSeat(DetectGreeterUser(), sudoPassword, logFunc)
ensureGreetdPamRundir(sudoPassword, logFunc)
if err := enableRunitService("greetd", sudoPassword); err != nil {
return fmt.Errorf("failed to enable greetd: %w", err)
}
logFunc(fmt.Sprintf("✓ greetd enabled (%s)", runitServiceDir))
return nil
}
state, err := checkSystemdEnabled("greetd")
if err != nil {
return fmt.Errorf("failed to check greetd state: %w", err)
@@ -2462,11 +2317,6 @@ func EnableGreetd(sudoPassword string, logFunc func(string)) error {
}
func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error {
if isRunit() {
logFunc("✓ runit detected; no graphical target is needed")
return nil
}
cmd := exec.Command("systemctl", "get-default")
output, err := cmd.Output()
if err != nil {
+1 -6
View File
@@ -236,18 +236,13 @@ func (r *Runner) Run() error {
r.log("Starting configuration deployment")
deployer := config.NewConfigDeployer(r.logChan)
useSystemd := true
if distroConfig, exists := distros.Registry[osInfo.Distribution.ID]; exists && distroConfig.Family == distros.FamilyVoid {
useSystemd = false
}
results, err := deployer.DeployConfigurationsSelectiveWithReinstallsAndSystemd(
results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(
context.Background(),
wm,
terminal,
dependencies,
replaceConfigs,
reinstallItems,
useSystemd,
)
if err != nil {
return fmt.Errorf("configuration deployment failed: %w", err)
+1 -1
View File
@@ -1154,7 +1154,7 @@ func readLuaOrHyprlangOverride(path string) (map[string]*hyprlandOverrideBind, e
if err != nil {
return nil, err
}
lines := expandLuaConfigLines(strings.Split(string(data), "\n"))
lines := strings.Split(string(data), "\n")
parser := NewHyprlandParser("")
pendingUnbinds := make(map[string]string)
for _, line := range lines {
@@ -1,414 +0,0 @@
package providers
import (
"maps"
"regexp"
"strconv"
"strings"
)
// Lua configs can express binds dynamically: variables (mainMod .. " + C"),
// tostring() calls, and numeric for loops (workspace binds). This resolves such
// expressions to literal key combos so the static bind parser can read them.
const luaMaxLoopIterations = 1000
var (
luaAssignRE = regexp.MustCompile(`^(?:local\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$`)
luaForRE = regexp.MustCompile(`^for\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(-?\d+)\s*,\s*(-?\d+)\s*(?:,\s*(-?\d+)\s*)?do\b(.*)$`)
luaTostringRE = regexp.MustCompile(`to(?:string|number)\s*\(\s*("(?:\\.|[^"])*"|'(?:\\.|[^'])*'|-?\d+(?:\.\d+)?)\s*\)`)
luaBlockOpenRE = regexp.MustCompile(`\b(?:function|for|while|if)\b`)
luaBlockCloseRE = regexp.MustCompile(`\bend\b`)
luaNumberRE = regexp.MustCompile(`^-?\d+(?:\.\d+)?$`)
)
type luaVarEnv map[string]string
type luaForHeader struct {
varName string
start int
stop int
step int
inline string
}
func expandLuaConfigLines(lines []string) []string {
return expandLuaBlock(lines, luaVarEnv{})
}
func expandLuaBlock(lines []string, env luaVarEnv) []string {
out := make([]string, 0, len(lines))
for i := 0; i < len(lines); i++ {
code := strings.TrimSpace(luaStripLineComment(lines[i]))
if name, value, ok := parseLuaStringAssignment(code, env); ok {
env[name] = value
out = append(out, lines[i])
continue
}
header, ok := parseLuaForHeader(code)
if !ok {
out = append(out, resolveLuaDynamicLine(lines[i], env))
continue
}
body, consumed, complete := collectLuaForBody(header, lines, i)
if !complete {
out = append(out, resolveLuaDynamicLine(lines[i], env))
continue
}
out = append(out, expandLuaForLoop(header, body, env)...)
i = consumed
}
return out
}
func parseLuaStringAssignment(code string, env luaVarEnv) (name, value string, ok bool) {
m := luaAssignRE.FindStringSubmatch(code)
if m == nil {
return "", "", false
}
value, ok = evalLuaConcat(m[2], env)
if !ok {
return "", "", false
}
return m[1], value, true
}
func parseLuaForHeader(code string) (luaForHeader, bool) {
m := luaForRE.FindStringSubmatch(code)
if m == nil {
return luaForHeader{}, false
}
start, _ := strconv.Atoi(m[2])
stop, _ := strconv.Atoi(m[3])
step := 1
if m[4] != "" {
step, _ = strconv.Atoi(m[4])
}
if step == 0 {
return luaForHeader{}, false
}
return luaForHeader{varName: m[1], start: start, stop: stop, step: step, inline: strings.TrimSpace(m[5])}, true
}
func collectLuaForBody(header luaForHeader, lines []string, headerIdx int) (body []string, consumed int, complete bool) {
depth := 1
if header.inline != "" {
delta := luaBlockDelta(header.inline)
if depth+delta <= 0 {
if stmt := strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(header.inline), "end")); stmt != "" {
body = append(body, stmt)
}
return body, headerIdx, true
}
depth += delta
body = append(body, header.inline)
}
for j := headerIdx + 1; j < len(lines); j++ {
delta := luaBlockDelta(lines[j])
if depth+delta <= 0 {
return body, j, true
}
depth += delta
body = append(body, lines[j])
}
return nil, headerIdx, false
}
func expandLuaForLoop(header luaForHeader, body []string, env luaVarEnv) []string {
var out []string
inRange := func(v int) bool {
if header.step > 0 {
return v <= header.stop
}
return v >= header.stop
}
count := 0
for v := header.start; inRange(v); v += header.step {
if count++; count > luaMaxLoopIterations {
break
}
value := strconv.Itoa(v)
iterLines := make([]string, len(body))
for k, bl := range body {
iterLines[k] = substituteLuaIdent(bl, header.varName, value)
}
out = append(out, expandLuaBlock(iterLines, cloneLuaEnv(env))...)
}
return out
}
func luaBlockDelta(line string) int {
masked := luaMaskStrings(line)
return len(luaBlockOpenRE.FindAllString(masked, -1)) - len(luaBlockCloseRE.FindAllString(masked, -1))
}
func resolveLuaDynamicLine(line string, env luaVarEnv) string {
if !strings.Contains(line, "hl.bind") && !strings.Contains(line, "hl.unbind") {
return line
}
line = normalizeLuaToString(line)
return rewriteLuaBindKeyArg(line, env)
}
func normalizeLuaToString(line string) string {
return luaTostringRE.ReplaceAllStringFunc(line, func(m string) string {
inner := luaTostringRE.FindStringSubmatch(m)[1]
if inner[0] == '"' || inner[0] == '\'' {
return inner
}
return strconv.Quote(inner)
})
}
func rewriteLuaBindKeyArg(line string, env luaVarEnv) string {
for _, fn := range []string{"hl.bind", "hl.unbind"} {
idx := strings.Index(line, fn)
if idx < 0 {
continue
}
open := skipLuaWS(line, idx+len(fn))
if open >= len(line) || line[open] != '(' {
continue
}
argStart := skipLuaWS(line, open+1)
expr, end, ok := parseLuaFirstArgExpr(line, argStart)
if !ok || isLuaPlainStringArg(expr) {
continue
}
value, ok := evalLuaConcat(expr, env)
if !ok {
continue
}
return line[:argStart] + strconv.Quote(value) + line[end:]
}
return line
}
func evalLuaConcat(expr string, env luaVarEnv) (string, bool) {
parts := splitLuaConcat(expr)
var sb strings.Builder
for _, part := range parts {
value, ok := evalLuaOperand(part, env)
if !ok {
return "", false
}
sb.WriteString(value)
}
return sb.String(), true
}
func evalLuaOperand(op string, env luaVarEnv) (string, bool) {
op = strings.TrimSpace(op)
if op == "" {
return "", false
}
switch op[0] {
case '"', '\'':
s, next, ok := parseLuaStringLiteral(op, 0)
return s, next == len(op) && ok
}
if luaNumberRE.MatchString(op) {
return op, true
}
if inner, ok := luaUnwrapCall(op, "tostring"); ok {
return evalLuaConcat(inner, env)
}
if inner, ok := luaUnwrapCall(op, "tonumber"); ok {
return evalLuaConcat(inner, env)
}
if value, ok := env[op]; ok {
return value, true
}
return "", false
}
func splitLuaConcat(expr string) []string {
var parts []string
parenDepth, braceDepth, bracketDepth := 0, 0, 0
inStr := byte(0)
esc := false
start := 0
for i := 0; i < len(expr); i++ {
c := expr[i]
if inStr != 0 {
switch {
case esc:
esc = false
case c == '\\' && inStr == '"':
esc = true
case c == inStr:
inStr = 0
}
continue
}
switch c {
case '"', '\'':
inStr = c
case '(':
parenDepth++
case ')':
if parenDepth > 0 {
parenDepth--
}
case '{':
braceDepth++
case '}':
if braceDepth > 0 {
braceDepth--
}
case '[':
bracketDepth++
case ']':
if bracketDepth > 0 {
bracketDepth--
}
case '.':
if parenDepth == 0 && braceDepth == 0 && bracketDepth == 0 && i+1 < len(expr) && expr[i+1] == '.' {
parts = append(parts, expr[start:i])
i++
start = i + 1
}
}
}
return append(parts, expr[start:])
}
func substituteLuaIdent(line, name, value string) string {
if !strings.Contains(line, name) {
return line
}
var sb strings.Builder
inStr := byte(0)
esc := false
for i := 0; i < len(line); {
c := line[i]
if inStr != 0 {
sb.WriteByte(c)
switch {
case esc:
esc = false
case c == '\\' && inStr == '"':
esc = true
case c == inStr:
inStr = 0
}
i++
continue
}
if c == '"' || c == '\'' {
inStr = c
sb.WriteByte(c)
i++
continue
}
if isLuaIdentStart(c) {
j := i + 1
for j < len(line) && isLuaIdentByte(line[j]) {
j++
}
word := line[i:j]
if word == name && (i == 0 || line[i-1] != '.') {
sb.WriteString(value)
} else {
sb.WriteString(word)
}
i = j
continue
}
sb.WriteByte(c)
i++
}
return sb.String()
}
func luaMaskStrings(line string) string {
b := []byte(line)
inStr := byte(0)
esc := false
for i := 0; i < len(b); i++ {
c := b[i]
if inStr != 0 {
wasEnd := !esc && c == inStr
esc = !esc && c == '\\' && inStr == '"'
b[i] = ' '
if wasEnd {
inStr = 0
}
continue
}
switch c {
case '"', '\'':
inStr = c
b[i] = ' '
case '-':
if i+1 < len(b) && b[i+1] == '-' {
for ; i < len(b); i++ {
b[i] = ' '
}
}
}
}
return string(b)
}
func luaStripLineComment(line string) string {
inStr := byte(0)
esc := false
for i := 0; i+1 < len(line); i++ {
c := line[i]
if inStr != 0 {
switch {
case esc:
esc = false
case c == '\\' && inStr == '"':
esc = true
case c == inStr:
inStr = 0
}
continue
}
switch c {
case '"', '\'':
inStr = c
case '-':
if line[i+1] == '-' {
return line[:i]
}
}
}
return line
}
func luaUnwrapCall(op, fn string) (string, bool) {
op = strings.TrimSpace(op)
if !strings.HasPrefix(op, fn) {
return "", false
}
rest := strings.TrimSpace(op[len(fn):])
if !strings.HasPrefix(rest, "(") || !strings.HasSuffix(rest, ")") {
return "", false
}
return rest[1 : len(rest)-1], true
}
func isLuaPlainStringArg(expr string) bool {
expr = strings.TrimSpace(expr)
if expr == "" || (expr[0] != '"' && expr[0] != '\'') {
return false
}
_, next, ok := parseLuaStringLiteral(expr, 0)
return ok && next == len(expr)
}
func isLuaIdentStart(c byte) bool {
return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
func cloneLuaEnv(env luaVarEnv) luaVarEnv {
clone := make(luaVarEnv, len(env))
maps.Copy(clone, env)
return clone
}
@@ -1,134 +0,0 @@
package providers
import (
"strings"
"testing"
)
func TestExpandLuaConfigLinesVariableConcat(t *testing.T) {
lines := []string{
`local mainMod = "SUPER"`,
`hl.bind(mainMod .. " + C", hl.dsp.window.close())`,
`hl.bind(mainMod .. " + H", hl.dsp.focus({direction = "l"}))`,
`hl.bind("ALT + TAB", hl.dsp.window.cycle_next({}))`,
}
got := expandLuaConfigLines(lines)
want := []string{
`hl.bind("SUPER + C",`,
`hl.bind("SUPER + H",`,
`hl.bind("ALT + TAB",`,
}
joined := strings.Join(got, "\n")
for _, w := range want {
if !strings.Contains(joined, w) {
t.Errorf("expanded output missing %q\n---\n%s", w, joined)
}
}
}
func TestExpandLuaConfigLinesForLoop(t *testing.T) {
lines := []string{
`local mainMod = "SUPER"`,
`for i = 1, 3 do`,
` hl.bind(mainMod .. " + " .. i, hl.dsp.focus({workspace = tostring(i)}))`,
` hl.bind(mainMod .. " SHIFT + " .. i, hl.dsp.window.move({workspace = tostring(i)}))`,
`end`,
}
got := strings.Join(expandLuaConfigLines(lines), "\n")
for _, w := range []string{
`hl.bind("SUPER + 1",`,
`hl.bind("SUPER + 2",`,
`hl.bind("SUPER + 3",`,
`hl.bind("SUPER SHIFT + 3",`,
`{workspace = "1"}`,
} {
if !strings.Contains(got, w) {
t.Errorf("expanded loop missing %q\n---\n%s", w, got)
}
}
}
func TestParseLuaLinesDynamicBinds(t *testing.T) {
content := strings.Join([]string{
`local mainMod = "SUPER"`,
`hl.bind(mainMod .. " + C", hl.dsp.window.close())`,
`hl.bind("ALT + TAB", hl.dsp.window.cycle_next({}))`,
`for i = 1, 2 do`,
` hl.bind(mainMod .. " + " .. i, hl.dsp.focus({workspace = tostring(i)}))`,
`end`,
}, "\n")
parser := NewHyprlandParser("")
section, err := parser.parseLuaLines(content, "", "test.lua", "")
if err != nil {
t.Fatalf("parseLuaLines: %v", err)
}
keys := map[string]*HyprlandKeyBinding{}
for i := range section.Keybinds {
kb := &section.Keybinds[i]
keys[parser.formatBindKey(kb)] = kb
}
for _, want := range []string{"SUPER+C", "ALT+TAB", "SUPER+1", "SUPER+2"} {
if _, ok := keys[want]; !ok {
t.Errorf("missing bind %q; got %v", want, keysList(keys))
}
}
if kb := keys["SUPER+C"]; kb != nil && kb.Dispatcher != "killactive" {
t.Errorf("SUPER+C dispatcher = %q, want killactive", kb.Dispatcher)
}
if kb := keys["SUPER+1"]; kb != nil {
if kb.Dispatcher != "workspace" || kb.Params != "1" {
t.Errorf("SUPER+1 = %q %q, want workspace 1", kb.Dispatcher, kb.Params)
}
}
}
func keysList(m map[string]*HyprlandKeyBinding) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
func TestEvalLuaConcat(t *testing.T) {
env := luaVarEnv{"mainMod": "SUPER", "i": "5"}
tests := []struct {
expr string
want string
ok bool
}{
{`mainMod .. " + C"`, "SUPER + C", true},
{`mainMod .. " + " .. i`, "SUPER + 5", true},
{`mainMod .. " + " .. tostring(i)`, "SUPER + 5", true},
{`"ALT + TAB"`, "ALT + TAB", true},
{`mainMod .. someFunc()`, "", false},
{`unknownVar .. "x"`, "", false},
}
for _, tt := range tests {
got, ok := evalLuaConcat(tt.expr, env)
if ok != tt.ok || (ok && got != tt.want) {
t.Errorf("evalLuaConcat(%q) = %q,%v want %q,%v", tt.expr, got, ok, tt.want, tt.ok)
}
}
}
func TestSubstituteLuaIdent(t *testing.T) {
tests := []struct {
line, name, value, want string
}{
{`hl.bind(m .. " + " .. i, x)`, "i", "3", `hl.bind(m .. " + " .. 3, x)`},
{`hl.dsp.exec("light -i")`, "i", "3", `hl.dsp.exec("light -i")`},
{`foo.i`, "i", "3", `foo.i`},
{`tostring(i)`, "i", "3", `tostring(3)`},
}
for _, tt := range tests {
if got := substituteLuaIdent(tt.line, tt.name, tt.value); got != tt.want {
t.Errorf("substituteLuaIdent(%q,%q,%q) = %q, want %q", tt.line, tt.name, tt.value, got, tt.want)
}
}
}
@@ -623,7 +623,7 @@ func (p *HyprlandParser) parseLuaLines(content string, baseDir, absPath, section
prevSource := p.currentSource
p.currentSource = absPath
lines := expandLuaConfigLines(strings.Split(content, "\n"))
lines := strings.Split(content, "\n")
boundInFile := make(map[string]bool)
for _, line := range lines {
trimmed := strings.TrimSpace(line)
+9 -36
View File
@@ -19,20 +19,6 @@ type NiriProvider struct {
parsed bool
}
type niriActionPart struct {
value string
quoted bool
}
var niriActionPropertyOrder = []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"}
var niriActionProperties = map[string]struct{}{
"focus": {},
"show-pointer": {},
"write-to-disk": {},
"skip-confirmation": {},
"delay-ms": {},
}
func NewNiriProvider(configDir string) *NiriProvider {
if configDir == "" {
configDir = defaultNiriConfigDir()
@@ -69,7 +55,6 @@ func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
sheet := &keybinds.CheatSheet{
Title: "Niri Keybinds",
Provider: n.Name(),
ModKey: result.ModKey,
Binds: categorizedBinds,
DMSBindsIncluded: result.DMSBindsIncluded,
}
@@ -367,7 +352,7 @@ func (n *NiriProvider) buildActionFromNode(bindNode *document.Node) string {
}
if actionNode.Properties != nil {
for _, propName := range niriActionPropertyOrder {
for _, propName := range []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"} {
if val, ok := actionNode.Properties.Get(propName); ok {
parts = append(parts, propName+"="+val.String())
}
@@ -455,10 +440,10 @@ func (n *NiriProvider) buildActionNode(action string) *document.Node {
return node
}
node.SetName(parts[0].value)
node.SetName(parts[0])
for _, arg := range parts[1:] {
if n.isNiriActionPropertyToken(arg) {
kv := strings.SplitN(arg.value, "=", 2)
if strings.Contains(arg, "=") {
kv := strings.SplitN(arg, "=", 2)
switch kv[1] {
case "true":
node.AddProperty(kv[0], true, "")
@@ -469,25 +454,13 @@ func (n *NiriProvider) buildActionNode(action string) *document.Node {
}
continue
}
node.AddArgument(arg.value, "")
node.AddArgument(arg, "")
}
return node
}
func (n *NiriProvider) isNiriActionPropertyToken(part niriActionPart) bool {
if part.quoted || !strings.Contains(part.value, "=") {
return false
}
key, _, ok := strings.Cut(part.value, "=")
if !ok {
return false
}
_, ok = niriActionProperties[key]
return ok
}
func (n *NiriProvider) parseActionParts(action string) []niriActionPart {
var parts []niriActionPart
func (n *NiriProvider) parseActionParts(action string) []string {
var parts []string
var current strings.Builder
var inQuote, escaped, wasQuoted bool
@@ -503,7 +476,7 @@ func (n *NiriProvider) parseActionParts(action string) []niriActionPart {
inQuote = !inQuote
case r == ' ' && !inQuote:
if current.Len() > 0 || wasQuoted {
parts = append(parts, niriActionPart{value: current.String(), quoted: wasQuoted})
parts = append(parts, current.String())
current.Reset()
wasQuoted = false
}
@@ -512,7 +485,7 @@ func (n *NiriProvider) parseActionParts(action string) []niriActionPart {
}
}
if current.Len() > 0 || wasQuoted {
parts = append(parts, niriActionPart{value: current.String(), quoted: wasQuoted})
parts = append(parts, current.String())
}
return parts
}
@@ -33,7 +33,6 @@ type NiriSection struct {
type NiriParser struct {
configDir string
modKey string
processedFiles map[string]bool
bindMap map[string]*NiriKeyBinding
bindOrder []string
@@ -238,7 +237,6 @@ func isBraceAdjacentSpace(b byte) bool {
func NewNiriParser(configDir string) *NiriParser {
return &NiriParser{
configDir: configDir,
modKey: "Super",
processedFiles: make(map[string]bool),
bindMap: make(map[string]*NiriKeyBinding),
bindOrder: []string{},
@@ -379,8 +377,6 @@ func (p *NiriParser) processNodes(nodes []*document.Node, section *NiriSection,
switch name {
case "include":
p.handleInclude(node, section, baseDir)
case "input":
p.handleInput(node)
case "binds":
p.extractBinds(node, section, "")
case "recent-windows":
@@ -389,19 +385,6 @@ func (p *NiriParser) processNodes(nodes []*document.Node, section *NiriSection,
}
}
func (p *NiriParser) handleInput(node *document.Node) {
for _, child := range node.Children {
if child.Name.String() != "mod-key" || len(child.Arguments) == 0 {
continue
}
modKey := strings.Trim(strings.TrimSpace(child.Arguments[0].String()), "\"")
if modKey != "" {
p.modKey = modKey
}
}
}
func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, baseDir string) {
if len(node.Arguments) == 0 {
return
@@ -551,7 +534,6 @@ func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) {
type NiriParseResult struct {
Section *NiriSection
ModKey string
DMSBindsIncluded bool
DMSStatus *DMSBindsStatusInfo
ConflictingConfigs map[string]*NiriKeyBinding
@@ -604,7 +586,6 @@ func ParseNiriKeys(configDir string) (*NiriParseResult, error) {
}
return &NiriParseResult{
Section: section,
ModKey: parser.modKey,
DMSBindsIncluded: parser.HasDMSBindsIncluded(),
DMSStatus: parser.buildDMSStatus(),
ConflictingConfigs: parser.conflictingConfigs,
@@ -7,28 +7,6 @@ import (
"testing"
)
func TestNiriParseModKey(t *testing.T) {
config := `input {
mod-key "Alt"
}
binds {
Mod+T { spawn "kitty"; }
}
`
tmpDir := t.TempDir()
if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0o644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if result.ModKey != "Alt" {
t.Errorf("ModKey = %q, want %q", result.ModKey, "Alt")
}
}
func TestNiriParse_NoSpaceBeforeBrace(t *testing.T) {
config := `recent-windows {
binds {
+1 -60
View File
@@ -17,10 +17,7 @@ func TestNiriProviderGetCheatSheet(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `input {
mod-key "Alt"
}
binds {
content := `binds {
Mod+Q { close-window; }
Mod+F { fullscreen-window; }
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
@@ -48,10 +45,6 @@ binds {
t.Errorf("Provider = %q, want %q", cheatSheet.Provider, "niri")
}
if cheatSheet.ModKey != "Alt" {
t.Errorf("ModKey = %q, want %q", cheatSheet.ModKey, "Alt")
}
windowBinds := cheatSheet.Binds["Window"]
if len(windowBinds) < 2 {
t.Errorf("Expected at least 2 Window binds, got %d", len(windowBinds))
@@ -234,58 +227,6 @@ func TestNiriGenerateBindsContent(t *testing.T) {
expected: `binds {
Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; }
}
`,
},
{
name: "spawn with equals arg",
binds: map[string]*overrideBind{
"Mod+B": {
Key: "Mod+B",
Action: `spawn /opt/browser --profile-directory=Default`,
},
},
expected: `binds {
Mod+B { spawn "/opt/browser" "--profile-directory=Default"; }
}
`,
},
{
name: "spawn shell command with quoted equals args",
binds: map[string]*overrideBind{
"Mod+C": {
Key: "Mod+C",
Action: `spawn sh -c "chrome --profile-directory=Default --app=x"`,
},
},
expected: `binds {
Mod+C { spawn "sh" "-c" "chrome --profile-directory=Default --app=x"; }
}
`,
},
{
name: "spawn env assignment stays arg",
binds: map[string]*overrideBind{
"Mod+E": {
Key: "Mod+E",
Action: `spawn env FOO=bar mycmd`,
},
},
expected: `binds {
Mod+E { spawn "env" "FOO=bar" "mycmd"; }
}
`,
},
{
name: "niri action property remains property",
binds: map[string]*overrideBind{
"Print": {
Key: "Print",
Action: `screenshot show-pointer=false`,
},
},
expected: `binds {
Print { screenshot show-pointer=false; }
}
`,
},
{
-1
View File
@@ -32,7 +32,6 @@ type DMSBindsStatus struct {
type CheatSheet struct {
Title string `json:"title"`
Provider string `json:"provider"`
ModKey string `json:"modKey,omitempty"`
Binds map[string][]Keybind `json:"binds"`
DMSBindsIncluded bool `json:"dmsBindsIncluded"`
DMSStatus *DMSBindsStatus `json:"dmsStatus,omitempty"`
+3 -5
View File
@@ -68,8 +68,6 @@ func GetQtLoggingRules() string {
level = "info"
}
// scene carries QML engine warnings (e.g. QQuickImage "Cannot open" cache
// probes); suppressed except at debug level
var rules []string
switch strings.ToLower(level) {
case "fatal":
@@ -77,13 +75,13 @@ func GetQtLoggingRules() string {
case "error":
rules = []string{"*.debug=false", "*.info=false", "*.warning=false"}
case "warn", "warning":
rules = []string{"*.debug=false", "*.info=false", "scene.warning=false"}
rules = []string{"*.debug=false", "*.info=false"}
case "info":
rules = []string{"*.debug=false", "scene.warning=false"}
rules = []string{"*.debug=false"}
case "debug":
return ""
default:
rules = []string{"*.debug=false", "scene.warning=false"}
rules = []string{"*.debug=false"}
}
return strings.Join(rules, ";")
+1 -73
View File
@@ -118,50 +118,6 @@ type ColorsOutput struct {
} `json:"colors"`
}
type SchemePreview struct {
Dark string `json:"dark"`
Light string `json:"light"`
}
var previewSchemeTypes = []string{
"scheme-tonal-spot",
"scheme-vibrant",
"scheme-content",
"scheme-expressive",
"scheme-fidelity",
"scheme-fruit-salad",
"scheme-monochrome",
"scheme-neutral",
"scheme-rainbow",
}
func PreviewSchemes(sourceColor string, contrast float64) (map[string]SchemePreview, error) {
if sourceColor == "" {
return nil, fmt.Errorf("source color is required")
}
previews := make(map[string]SchemePreview, len(previewSchemeTypes))
for _, schemeType := range previewSchemeTypes {
output, err := runMatugenDryRun(&Options{
Kind: "hex",
Value: sourceColor,
MatugenType: schemeType,
Contrast: contrast,
})
if err != nil {
return nil, fmt.Errorf("preview %s: %w", schemeType, err)
}
dark := extractMatugenColor(output, "primary", "dark")
light := extractMatugenColor(output, "primary", "light")
if dark == "" || light == "" {
return nil, fmt.Errorf("preview %s: primary colors missing from matugen output", schemeType)
}
previews[schemeType] = SchemePreview{Dark: dark, Light: light}
}
return previews, nil
}
func (o *Options) ColorsOutput() string {
return filepath.Join(o.StateDir, "dms-colors.json")
}
@@ -699,7 +655,6 @@ func redetectMatugenVersion(old matugenFlags) (matugenFlags, bool) {
func detectMatugenVersionLocked() (matugenFlags, error) {
cmd := exec.Command("matugen", "--version")
cmd.Env = utils.EnvWithUserBinPath(nil)
output, err := cmd.Output()
if err != nil {
return matugenFlags{}, fmt.Errorf("failed to get matugen version: %w", err)
@@ -756,7 +711,6 @@ func runMatugen(baseArgs []string) error {
args := buildMatugenArgs(baseArgs, flags)
cmd := exec.Command("matugen", args...)
cmd.Env = utils.EnvWithUserBinPath(nil)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
runErr := cmd.Run()
@@ -774,7 +728,6 @@ func runMatugen(baseArgs []string) error {
log.Warnf("Matugen version changed (v4: %v -> %v), retrying", flags.isV4, newFlags.isV4)
args = buildMatugenArgs(baseArgs, newFlags)
retryCmd := exec.Command("matugen", args...)
retryCmd.Env = utils.EnvWithUserBinPath(nil)
retryCmd.Stdout = os.Stdout
retryCmd.Stderr = os.Stderr
return retryCmd.Run()
@@ -817,7 +770,6 @@ func execDryRun(opts *Options, flags matugenFlags) (string, error) {
}
cmd := exec.Command("matugen", baseArgs...)
cmd.Env = utils.EnvWithUserBinPath(nil)
var stderr strings.Builder
cmd.Stderr = &stderr
output, err := cmd.Output()
@@ -916,26 +868,7 @@ func refreshGTK(mode ColorMode) {
}
}
var colorSchemeEchoHook func(scheme string)
func SetColorSchemeEchoHook(hook func(scheme string)) {
colorSchemeEchoHook = hook
}
func expectColorSchemeEcho(scheme string) {
if colorSchemeEchoHook != nil {
colorSchemeEchoHook(scheme)
}
}
// The color-scheme round trip is the only mechanism that makes running GTK4
// apps reload ~/.config/gtk-4.0 CSS (a gtk-theme flip does not). But apps
// following the portal color-scheme (Chromium) can drop the restore signal
// mid-repaint and latch the wrong mode, so this is opt-in.
func refreshGTK4() {
if os.Getenv("DMS_ENABLE_GTK4_REFRESH") != "1" {
return
}
output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
if err != nil {
return
@@ -949,13 +882,11 @@ func refreshGTK4() {
toggle = "prefer-dark"
}
expectColorSchemeEcho(toggle)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil {
log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err)
return
}
time.Sleep(400 * time.Millisecond)
expectColorSchemeEcho(current)
time.Sleep(50 * time.Millisecond)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil {
log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err)
}
@@ -1056,9 +987,6 @@ func closestAdwaitaAccent(primaryHex string) string {
func syncAccentColor(primaryHex string) {
accent := closestAdwaitaAccent(primaryHex)
if cur, err := utils.GsettingsGet("org.gnome.desktop.interface", "accent-color"); err == nil && strings.Trim(cur, "'") == accent {
return
}
log.Infof("Setting GNOME accent color: %s", accent)
if err := utils.GsettingsSet("org.gnome.desktop.interface", "accent-color", accent); err != nil {
log.Warnf("Failed to set accent-color: %v", err)
+8 -23
View File
@@ -554,50 +554,35 @@ func (m *Manager) findInDirByIDOrName(dir, idOrName string) (string, error) {
return "", nil
}
func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (hasUpdates bool, diffURL string, err error) {
func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (bool, error) {
pluginPath, err := m.findInstalledPath(pluginID)
if err != nil {
return false, "", fmt.Errorf("failed to find plugin: %w", err)
return false, fmt.Errorf("failed to find plugin: %w", err)
}
if pluginPath == "" {
return false, "", fmt.Errorf("plugin not installed: %s", pluginID)
return false, fmt.Errorf("plugin not installed: %s", pluginID)
}
if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
return false, "", nil
return false, nil
}
metaPath := pluginPath + ".meta"
metaExists, err := afero.Exists(m.fs, metaPath)
if err != nil {
return false, "", fmt.Errorf("failed to check metadata: %w", err)
return false, fmt.Errorf("failed to check metadata: %w", err)
}
var hasUp bool
var localHash, remoteHash string
if metaExists {
// Plugin is from a monorepo, check the repo directory
reposDir := filepath.Join(m.pluginsDir, ".repos")
repoName := m.getRepoName(plugin.Repo)
repoPath := filepath.Join(reposDir, repoName)
hasUp, localHash, remoteHash, err = m.gitClient.HasUpdates(repoPath)
} else {
// Plugin is a standalone repo
hasUp, localHash, remoteHash, err = m.gitClient.HasUpdates(pluginPath)
return m.gitClient.HasUpdates(repoPath)
}
if err != nil {
return false, "", err
}
diffURL = plugin.Repo
if diffURL != "" {
diffURL = strings.TrimSuffix(diffURL, ".git")
if hasUp && len(localHash) >= 7 && len(remoteHash) >= 7 {
diffURL = fmt.Sprintf("%s/compare/%s...%s", diffURL, localHash[:7], remoteHash[:7])
}
}
return hasUp, diffURL, nil
// Plugin is a standalone repo
return m.gitClient.HasUpdates(pluginPath)
}
+9 -10
View File
@@ -33,7 +33,7 @@ type Plugin struct {
type GitClient interface {
PlainClone(path string, url string) error
Pull(path string) error
HasUpdates(path string) (hasUpdates bool, localHash string, remoteHash string, err error)
HasUpdates(path string) (bool, error)
}
type realGitClient struct{}
@@ -65,10 +65,10 @@ func (g *realGitClient) Pull(path string) error {
return nil
}
func (g *realGitClient) HasUpdates(path string) (bool, string, string, error) {
func (g *realGitClient) HasUpdates(path string) (bool, error) {
repo, err := git.PlainOpen(path)
if err != nil {
return false, "", "", err
return false, err
}
// Fetch remote changes
@@ -76,24 +76,24 @@ func (g *realGitClient) HasUpdates(path string) (bool, string, string, error) {
if err != nil && err.Error() != "already up-to-date" {
// If fetch fails, we can't determine if there are updates
// Return false and the error
return false, "", "", err
return false, err
}
// Get the HEAD reference
head, err := repo.Head()
if err != nil {
return false, "", "", err
return false, err
}
// Get the remote HEAD reference (typically origin/HEAD or origin/main or origin/master)
remote, err := repo.Remote("origin")
if err != nil {
return false, "", "", err
return false, err
}
refs, err := remote.List(&git.ListOptions{})
if err != nil {
return false, "", "", err
return false, err
}
// Find the default branch remote ref
@@ -108,14 +108,13 @@ func (g *realGitClient) HasUpdates(path string) (bool, string, string, error) {
}
}
localHash := head.Hash().String()
// If we couldn't find a remote HEAD, assume no updates
if remoteHead == "" {
return false, localHash, "", nil
return false, nil
}
// Compare local HEAD with remote HEAD
return localHash != remoteHead, localHash, remoteHead, nil
return head.Hash().String() != remoteHead, nil
}
type Registry struct {
+3 -3
View File
@@ -13,7 +13,7 @@ import (
type mockGitClient struct {
cloneFunc func(path string, url string) error
pullFunc func(path string) error
hasUpdatesFunc func(path string) (bool, string, string, error)
hasUpdatesFunc func(path string) (bool, error)
}
func (m *mockGitClient) PlainClone(path string, url string) error {
@@ -30,11 +30,11 @@ func (m *mockGitClient) Pull(path string) error {
return nil
}
func (m *mockGitClient) HasUpdates(path string) (bool, string, string, error) {
func (m *mockGitClient) HasUpdates(path string) (bool, error) {
if m.hasUpdatesFunc != nil {
return m.hasUpdatesFunc(path)
}
return false, "", "", nil
return false, nil
}
func TestNewRegistry(t *testing.T) {
@@ -1,289 +0,0 @@
// Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
// XML file : internal/proto/xml/virtual-keyboard-unstable-v1.xml
//
// virtual_keyboard_unstable_v1 Protocol Copyright:
//
// Copyright © 2008-2011 Kristian Høgsberg
// Copyright © 2010-2013 Intel Corporation
// Copyright © 2012-2013 Collabora, Ltd.
// Copyright © 2018 Purism SPC
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice (including the next
// paragraph) shall be included in all copies or substantial portions of the
// Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
package virtual_keyboard
import (
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"golang.org/x/sys/unix"
)
// ZwpVirtualKeyboardV1InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZwpVirtualKeyboardV1InterfaceName = "zwp_virtual_keyboard_v1"
// ZwpVirtualKeyboardV1 : virtual keyboard
//
// The virtual keyboard provides an application with requests which emulate
// the behaviour of a physical keyboard.
//
// This interface can be used by clients on its own to provide raw input
// events, or it can accompany the input method protocol.
type ZwpVirtualKeyboardV1 struct {
client.BaseProxy
}
// NewZwpVirtualKeyboardV1 : virtual keyboard
//
// The virtual keyboard provides an application with requests which emulate
// the behaviour of a physical keyboard.
//
// This interface can be used by clients on its own to provide raw input
// events, or it can accompany the input method protocol.
func NewZwpVirtualKeyboardV1(ctx *client.Context) *ZwpVirtualKeyboardV1 {
zwpVirtualKeyboardV1 := &ZwpVirtualKeyboardV1{}
ctx.Register(zwpVirtualKeyboardV1)
return zwpVirtualKeyboardV1
}
// Keymap : keyboard mapping
//
// Provide a file descriptor to the compositor which can be
// memory-mapped to provide a keyboard mapping description.
//
// Format carries a value from the keymap_format enumeration.
//
// format: keymap format
// fd: keymap file descriptor
// size: keymap size, in bytes
func (i *ZwpVirtualKeyboardV1) Keymap(format uint32, fd int, size uint32) error {
const opcode = 0
const _reqBufLen = 8 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(format))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(size))
l += 4
oob := unix.UnixRights(int(fd))
err := i.Context().WriteMsg(_reqBuf[:], oob)
return err
}
// Key : key event
//
// A key was pressed or released.
// The time argument is a timestamp with millisecond granularity, with an
// undefined base. All requests regarding a single object must share the
// same clock.
//
// Keymap must be set before issuing this request.
//
// State carries a value from the key_state enumeration.
//
// time: timestamp with millisecond granularity
// key: key that produced the event
// state: physical state of the key
func (i *ZwpVirtualKeyboardV1) Key(time, key, state uint32) error {
const opcode = 1
const _reqBufLen = 8 + 4 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(time))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(key))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(state))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// Modifiers : modifier and group state
//
// Notifies the compositor that the modifier and/or group state has
// changed, and it should update state.
//
// The client should use wl_keyboard.modifiers event to synchronize its
// internal state with seat state.
//
// Keymap must be set before issuing this request.
//
// modsDepressed: depressed modifiers
// modsLatched: latched modifiers
// modsLocked: locked modifiers
// group: keyboard layout
func (i *ZwpVirtualKeyboardV1) Modifiers(modsDepressed, modsLatched, modsLocked, group uint32) error {
const opcode = 2
const _reqBufLen = 8 + 4 + 4 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(modsDepressed))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(modsLatched))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(modsLocked))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(group))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// Destroy : destroy the virtual keyboard keyboard object
func (i *ZwpVirtualKeyboardV1) Destroy() error {
defer i.Context().Unregister(i)
const opcode = 3
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
type ZwpVirtualKeyboardV1Error uint32
// ZwpVirtualKeyboardV1Error :
const (
// ZwpVirtualKeyboardV1ErrorNoKeymap : No keymap was set
ZwpVirtualKeyboardV1ErrorNoKeymap ZwpVirtualKeyboardV1Error = 0
)
func (e ZwpVirtualKeyboardV1Error) Name() string {
switch e {
case ZwpVirtualKeyboardV1ErrorNoKeymap:
return "no_keymap"
default:
return ""
}
}
func (e ZwpVirtualKeyboardV1Error) Value() string {
switch e {
case ZwpVirtualKeyboardV1ErrorNoKeymap:
return "0"
default:
return ""
}
}
func (e ZwpVirtualKeyboardV1Error) String() string {
return e.Name() + "=" + e.Value()
}
// ZwpVirtualKeyboardManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZwpVirtualKeyboardManagerV1InterfaceName = "zwp_virtual_keyboard_manager_v1"
// ZwpVirtualKeyboardManagerV1 : virtual keyboard manager
//
// A virtual keyboard manager allows an application to provide keyboard
// input events as if they came from a physical keyboard.
type ZwpVirtualKeyboardManagerV1 struct {
client.BaseProxy
}
// NewZwpVirtualKeyboardManagerV1 : virtual keyboard manager
//
// A virtual keyboard manager allows an application to provide keyboard
// input events as if they came from a physical keyboard.
func NewZwpVirtualKeyboardManagerV1(ctx *client.Context) *ZwpVirtualKeyboardManagerV1 {
zwpVirtualKeyboardManagerV1 := &ZwpVirtualKeyboardManagerV1{}
ctx.Register(zwpVirtualKeyboardManagerV1)
return zwpVirtualKeyboardManagerV1
}
// CreateVirtualKeyboard : Create a new virtual keyboard
//
// Creates a new virtual keyboard associated to a seat.
//
// If the compositor enables a keyboard to perform arbitrary actions, it
// should present an error when an untrusted client requests a new
// keyboard.
func (i *ZwpVirtualKeyboardManagerV1) CreateVirtualKeyboard(seat *client.Seat) (*ZwpVirtualKeyboardV1, error) {
id := NewZwpVirtualKeyboardV1(i.Context())
const opcode = 0
const _reqBufLen = 8 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], seat.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], id.ID())
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return id, err
}
func (i *ZwpVirtualKeyboardManagerV1) Destroy() error {
i.Context().Unregister(i)
return nil
}
type ZwpVirtualKeyboardManagerV1Error uint32
// ZwpVirtualKeyboardManagerV1Error :
const (
// ZwpVirtualKeyboardManagerV1ErrorUnauthorized : client not authorized to use the interface
ZwpVirtualKeyboardManagerV1ErrorUnauthorized ZwpVirtualKeyboardManagerV1Error = 0
)
func (e ZwpVirtualKeyboardManagerV1Error) Name() string {
switch e {
case ZwpVirtualKeyboardManagerV1ErrorUnauthorized:
return "unauthorized"
default:
return ""
}
}
func (e ZwpVirtualKeyboardManagerV1Error) Value() string {
switch e {
case ZwpVirtualKeyboardManagerV1ErrorUnauthorized:
return "0"
default:
return ""
}
}
func (e ZwpVirtualKeyboardManagerV1Error) String() string {
return e.Name() + "=" + e.Value()
}
@@ -1,113 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="virtual_keyboard_unstable_v1">
<copyright>
Copyright © 2008-2011 Kristian Høgsberg
Copyright © 2010-2013 Intel Corporation
Copyright © 2012-2013 Collabora, Ltd.
Copyright © 2018 Purism SPC
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</copyright>
<interface name="zwp_virtual_keyboard_v1" version="1">
<description summary="virtual keyboard">
The virtual keyboard provides an application with requests which emulate
the behaviour of a physical keyboard.
This interface can be used by clients on its own to provide raw input
events, or it can accompany the input method protocol.
</description>
<request name="keymap">
<description summary="keyboard mapping">
Provide a file descriptor to the compositor which can be
memory-mapped to provide a keyboard mapping description.
Format carries a value from the keymap_format enumeration.
</description>
<arg name="format" type="uint" summary="keymap format"/>
<arg name="fd" type="fd" summary="keymap file descriptor"/>
<arg name="size" type="uint" summary="keymap size, in bytes"/>
</request>
<enum name="error">
<entry name="no_keymap" value="0" summary="No keymap was set"/>
</enum>
<request name="key">
<description summary="key event">
A key was pressed or released.
The time argument is a timestamp with millisecond granularity, with an
undefined base. All requests regarding a single object must share the
same clock.
Keymap must be set before issuing this request.
State carries a value from the key_state enumeration.
</description>
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
<arg name="key" type="uint" summary="key that produced the event"/>
<arg name="state" type="uint" summary="physical state of the key"/>
</request>
<request name="modifiers">
<description summary="modifier and group state">
Notifies the compositor that the modifier and/or group state has
changed, and it should update state.
The client should use wl_keyboard.modifiers event to synchronize its
internal state with seat state.
Keymap must be set before issuing this request.
</description>
<arg name="mods_depressed" type="uint" summary="depressed modifiers"/>
<arg name="mods_latched" type="uint" summary="latched modifiers"/>
<arg name="mods_locked" type="uint" summary="locked modifiers"/>
<arg name="group" type="uint" summary="keyboard layout"/>
</request>
<request name="destroy" type="destructor" since="1">
<description summary="destroy the virtual keyboard keyboard object"/>
</request>
</interface>
<interface name="zwp_virtual_keyboard_manager_v1" version="1">
<description summary="virtual keyboard manager">
A virtual keyboard manager allows an application to provide keyboard
input events as if they came from a physical keyboard.
</description>
<enum name="error">
<entry name="unauthorized" value="0" summary="client not authorized to use the interface"/>
</enum>
<request name="create_virtual_keyboard">
<description summary="Create a new virtual keyboard">
Creates a new virtual keyboard associated to a seat.
If the compositor enables a keyboard to perform arbitrary actions, it
should present an error when an untrusted client requests a new
keyboard.
</description>
<arg name="seat" type="object" interface="wl_seat"/>
<arg name="id" type="new_id" interface="zwp_virtual_keyboard_v1"/>
</request>
</interface>
</protocol>
+1 -8
View File
@@ -68,12 +68,6 @@ func (b *DDCBackend) scanI2CDevicesInternal(force bool) error {
activeBuses[i] = true
id := fmt.Sprintf("ddc:i2c-%d", i)
// Don't re-probe identified monitors: DDC traffic during a wake
// sequence can disturb some monitors' own brightness handling.
if _, ok := b.devices.Load(id); ok {
continue
}
dev, err := b.probeDDCDevice(i)
if err != nil || dev == nil {
continue
@@ -113,8 +107,7 @@ func (b *DDCBackend) probeDDCDevice(bus int) (*ddcDevice, error) {
dummy := make([]byte, 32)
syscall.Read(fd, dummy) //nolint:errcheck
writebuf := []byte{DDC_SOURCE_ADDR, 0x80}
writebuf = append(writebuf, ddcciChecksum(writebuf))
writebuf := []byte{0x00}
n, err := syscall.Write(fd, writebuf)
if err == nil && n == len(writebuf) {
name := b.getDDCName(bus)
@@ -6,7 +6,6 @@ import (
"fmt"
"net"
clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
@@ -29,10 +28,6 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
handleCopyEntry(conn, req, m)
case "clipboard.paste":
handlePaste(conn, req, m)
case "clipboard.sendPaste":
handleSendPaste(conn, req)
case "clipboard.pasteSupported":
models.Respond(conn, req.ID, map[string]bool{"supported": m.pasteSupported})
case "clipboard.subscribe":
handleSubscribe(conn, req, m)
case "clipboard.search":
@@ -181,17 +176,6 @@ func handlePaste(conn net.Conn, req models.Request, m *Manager) {
models.Respond(conn, req.ID, map[string]string{"text": text})
}
func handleSendPaste(conn net.Conn, req models.Request) {
shift, _ := models.Get[bool](req, "shift")
if err := clipboardstore.SendPasteKeystroke(shift); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "paste sent"})
}
func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
clientID := fmt.Sprintf("clipboard-%d", req.ID)
+76 -31
View File
@@ -31,7 +31,6 @@ import (
clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/virtual_keyboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
@@ -165,8 +164,6 @@ func (m *Manager) setupRegistry() error {
m.seat = seat
m.seatName = e.Name
log.Info("Bound wl_seat")
case virtual_keyboard.ZwpVirtualKeyboardManagerV1InterfaceName:
m.pasteSupported = true
}
})
@@ -1057,13 +1054,6 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
dataCopy := make([]byte, len(data))
copy(dataCopy, data)
m.takeSelection(clipboardstore.ExpandOffers(dataCopy, mimeType))
return nil
}
// takeSelection makes the daemon the selection owner, serving the given
// offers until another client claims the clipboard.
func (m *Manager) takeSelection(offers []clipboardstore.Offer) {
m.post(func() {
if m.dataControlMgr == nil || m.dataDevice == nil {
log.Error("Data control manager or device not initialized")
@@ -1078,13 +1068,9 @@ func (m *Manager) takeSelection(offers []clipboardstore.Offer) {
return
}
offerData := make(map[string][]byte, len(offers))
for _, offer := range offers {
if err := source.Offer(offer.MimeType); err != nil {
log.Errorf("Failed to offer %s: %v", offer.MimeType, err)
return
}
offerData[offer.MimeType] = offer.Data
if err := source.Offer(mimeType); err != nil {
log.Errorf("Failed to offer mime type: %v", err)
return
}
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
@@ -1094,11 +1080,7 @@ func (m *Manager) takeSelection(offers []clipboardstore.Offer) {
file := os.NewFile(uintptr(fd), "clipboard-pipe")
defer file.Close()
data, ok := offerData[e.MimeType]
if !ok {
return
}
if _, err := file.Write(data); err != nil {
if _, err := file.Write(dataCopy); err != nil {
log.Errorf("Failed to write clipboard data: %v", err)
}
})
@@ -1111,6 +1093,9 @@ func (m *Manager) takeSelection(offers []clipboardstore.Offer) {
m.releaseCurrentSource()
m.currentSource = source
m.sourceMutex.Lock()
m.sourceMimeTypes = []string{mimeType}
m.sourceMutex.Unlock()
m.ownerLock.Lock()
m.isOwner = true
@@ -1121,6 +1106,8 @@ func (m *Manager) takeSelection(offers []clipboardstore.Offer) {
log.Errorf("Failed to set selection: %v", err)
}
})
return nil
}
func (m *Manager) CopyText(text string) error {
@@ -1792,16 +1779,74 @@ func (m *Manager) CopyFile(filePath string) error {
m.updateState()
m.notifySubscribers()
offers := []clipboardstore.Offer{
{MimeType: "x-special/gnome-copied-files", Data: []byte("copy\n" + fileURI)},
{MimeType: "text/uri-list", Data: []byte(fileURI + "\r\n")},
{MimeType: "text/plain", Data: []byte(filePath)},
}
if _, imgMime, err := image.DecodeConfig(bytes.NewReader(fileData)); err == nil {
offers = append(offers, clipboardstore.Offer{MimeType: "image/" + imgMime, Data: fileData})
}
_, imgMime, imgErr := image.DecodeConfig(bytes.NewReader(fileData))
m.post(func() {
if m.dataControlMgr == nil || m.dataDevice == nil {
log.Error("Data control manager or device not initialized")
return
}
dataMgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1)
source, err := dataMgr.CreateDataSource()
if err != nil {
log.Errorf("Failed to create data source: %v", err)
return
}
type offer struct {
mime string
data []byte
}
offers := []offer{
{"x-special/gnome-copied-files", []byte("copy\n" + fileURI)},
{"text/uri-list", []byte(fileURI + "\r\n")},
{"text/plain", []byte(filePath)},
}
if imgErr == nil {
imgMimeType := "image/" + imgMime
offers = append(offers, offer{imgMimeType, fileData})
}
offerData := make(map[string][]byte)
for _, o := range offers {
if err := source.Offer(o.mime); err != nil {
log.Errorf("Failed to offer %s: %v", o.mime, err)
return
}
offerData[o.mime] = o.data
}
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
fd := e.Fd
defer syscall.Close(fd)
file := os.NewFile(uintptr(fd), "clipboard-pipe")
defer file.Close()
if data, ok := offerData[e.MimeType]; ok {
file.Write(data)
}
})
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
m.ownerLock.Lock()
m.isOwner = false
m.ownerLock.Unlock()
})
m.releaseCurrentSource()
m.currentSource = source
m.ownerLock.Lock()
m.isOwner = true
m.ownerLock.Unlock()
device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1)
if err := device.SetSelection(source); err != nil {
log.Errorf("Failed to set selection: %v", err)
}
})
m.takeSelection(offers)
return nil
}
@@ -531,6 +531,47 @@ func TestManager_ConcurrentOfferAccess(t *testing.T) {
wg.Wait()
}
func TestManager_ConcurrentPersistAccess(t *testing.T) {
m := &Manager{
persistData: make(map[string][]byte),
persistMimeTypes: []string{},
}
var wg sync.WaitGroup
const goroutines = 20
const iterations = 50
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
m.persistMutex.RLock()
_ = m.persistData
_ = m.persistMimeTypes
m.persistMutex.RUnlock()
}
}()
}
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < iterations; j++ {
m.persistMutex.Lock()
m.persistMimeTypes = []string{"text/plain", "text/html"}
m.persistData = map[string][]byte{
"text/plain": []byte("test"),
}
m.persistMutex.Unlock()
}
}(i)
}
wg.Wait()
}
func TestManager_ConcurrentOwnerAccess(t *testing.T) {
m := &Manager{}
+9 -3
View File
@@ -132,9 +132,15 @@ type Manager struct {
offerMutex sync.RWMutex
offerRegistry map[uint32]any
isOwner bool
ownerLock sync.Mutex
pasteSupported bool
sourceMimeTypes []string
sourceMutex sync.RWMutex
persistData map[string][]byte
persistMimeTypes []string
persistMutex sync.RWMutex
isOwner bool
ownerLock sync.Mutex
initialized bool
+1 -39
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"os"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
@@ -101,41 +100,6 @@ func (m *Manager) initializeSettings() error {
return nil
}
// ExpectColorSchemeEcho registers a self-write so the watcher swallows its SettingChanged echo instead of forwarding it as an external change.
func (m *Manager) ExpectColorSchemeEcho(scheme string) {
var value uint32
switch scheme {
case "prefer-dark":
value = 1
case "prefer-light":
value = 2
}
m.selfEchoMu.Lock()
m.selfEchoes = append(m.selfEchoes, colorSchemeEcho{value: value, expires: time.Now().Add(10 * time.Second)})
m.selfEchoMu.Unlock()
}
func (m *Manager) consumeSelfEcho(value uint32) bool {
m.selfEchoMu.Lock()
defer m.selfEchoMu.Unlock()
now := time.Now()
kept := m.selfEchoes[:0]
consumed := false
for _, echo := range m.selfEchoes {
if now.After(echo.expires) {
continue
}
if !consumed && echo.value == value {
consumed = true
continue
}
kept = append(kept, echo)
}
m.selfEchoes = kept
return consumed
}
func (m *Manager) watchSettingsChanges() {
conn, err := dbus.ConnectSessionBus()
if err != nil {
@@ -178,15 +142,13 @@ func (m *Manager) watchSettingsChanges() {
continue
}
selfInflicted := m.consumeSelfEcho(colorScheme)
m.stateMutex.Lock()
changed := m.state.Settings.ColorScheme != colorScheme || !m.state.Settings.Available
m.state.Settings.ColorScheme = colorScheme
m.state.Settings.Available = true
m.stateMutex.Unlock()
if changed && !selfInflicted {
if changed {
m.NotifySubscribers()
}
}
@@ -141,45 +141,3 @@ func TestManager_SettingsState_Modification(t *testing.T) {
original := manager.GetState()
assert.Equal(t, uint32(0), original.Settings.ColorScheme)
}
func TestManager_SelfEcho_ConsumesRegisteredWrites(t *testing.T) {
manager := &Manager{state: &FreedeskState{}}
manager.ExpectColorSchemeEcho("prefer-dark")
manager.ExpectColorSchemeEcho("default")
assert.True(t, manager.consumeSelfEcho(1))
assert.True(t, manager.consumeSelfEcho(0))
assert.False(t, manager.consumeSelfEcho(1))
assert.False(t, manager.consumeSelfEcho(0))
}
func TestManager_SelfEcho_ExternalChangePassesThrough(t *testing.T) {
manager := &Manager{state: &FreedeskState{}}
manager.ExpectColorSchemeEcho("prefer-dark")
assert.False(t, manager.consumeSelfEcho(2))
assert.True(t, manager.consumeSelfEcho(1))
}
func TestManager_SelfEcho_ConsumesOnePerRegistration(t *testing.T) {
manager := &Manager{state: &FreedeskState{}}
manager.ExpectColorSchemeEcho("prefer-dark")
manager.ExpectColorSchemeEcho("prefer-dark")
assert.True(t, manager.consumeSelfEcho(1))
assert.True(t, manager.consumeSelfEcho(1))
assert.False(t, manager.consumeSelfEcho(1))
}
func TestManager_SelfEcho_SchemeMapping(t *testing.T) {
manager := &Manager{state: &FreedeskState{}}
manager.ExpectColorSchemeEcho("prefer-light")
assert.True(t, manager.consumeSelfEcho(2))
manager.ExpectColorSchemeEcho("default")
assert.True(t, manager.consumeSelfEcho(0))
}
@@ -2,17 +2,11 @@ package freedesktop
import (
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5"
)
type colorSchemeEcho struct {
value uint32
expires time.Time
}
type AccountsState struct {
Available bool `json:"available"`
UserPath string `json:"userPath"`
@@ -69,6 +63,4 @@ type Manager struct {
screensaverCookieCounter uint32
screensaverFreedesktopClaimed bool
screensaverGnomeClaimed bool
selfEchoMu sync.Mutex
selfEchoes []colorSchemeEcho
}
-13
View File
@@ -46,19 +46,6 @@ func (m *Manager) Activate() error {
return nil
}
func (m *Manager) SetLockedHint(locked bool) error {
err := m.sessionObj.Call(dbusSessionInterface+".SetLockedHint", 0, locked).Err
if err != nil {
if refreshErr := m.refreshSessionBinding(); refreshErr == nil {
err = m.sessionObj.Call(dbusSessionInterface+".SetLockedHint", 0, locked).Err
}
if err != nil {
return fmt.Errorf("failed to set locked hint: %w", err)
}
}
return nil
}
func (m *Manager) SetIdleHint(idle bool) error {
err := m.sessionObj.Call(dbusSessionInterface+".SetIdleHint", 0, idle).Err
if err != nil {
-16
View File
@@ -21,8 +21,6 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
handleActivate(conn, req, manager)
case "loginctl.setIdleHint":
handleSetIdleHint(conn, req, manager)
case "loginctl.setLockedHint":
handleSetLockedHint(conn, req, manager)
case "loginctl.setLockBeforeSuspend":
handleSetLockBeforeSuspend(conn, req, manager)
case "loginctl.setSleepInhibitorEnabled":
@@ -80,20 +78,6 @@ func handleSetIdleHint(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "idle hint set"})
}
func handleSetLockedHint(conn net.Conn, req models.Request, manager *Manager) {
locked, err := params.Bool(req.Params, "locked")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
if err := manager.SetLockedHint(locked); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "locked hint set"})
}
func handleSetLockBeforeSuspend(conn net.Conn, req models.Request, manager *Manager) {
enabled, err := params.Bool(req.Params, "enabled")
if err != nil {
@@ -545,18 +545,12 @@ func (a *SecretAgent) GetSecrets(
out[settingName] = sec
}
if settingName == "vpn" && a.backend != nil && !isPKCS11 && (vpnUsername != "" || reply.Save) {
secrets := make(map[string]string)
for k, v := range reply.Secrets {
if k != "username" {
secrets[k] = v
}
}
pw := reply.Secrets["password"]
a.backend.pendingVPNSaveMu.Lock()
a.backend.pendingVPNSave = &pendingVPNCredentials{
ConnectionPath: string(path),
Username: vpnUsername,
Password: reply.Secrets["password"],
Secrets: secrets,
Password: pw,
SavePassword: reply.Save,
}
a.backend.pendingVPNSaveMu.Unlock()
@@ -796,23 +790,7 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
fields = []string{"username", "password"}
}
case strings.Contains(vpnService, "openvpn"):
switch connType {
case "tls":
// Cert auth wants the private-key passphrase, not a password.
if secretFlagsNotRequired(dataMap["cert-pass-flags"]) {
return nil
}
return []string{"cert-pass"}
case "password-tls":
fields = []string{}
if dataMap["username"] == "" {
fields = append(fields, "username")
}
fields = append(fields, "password")
if !secretFlagsNotRequired(dataMap["cert-pass-flags"]) {
fields = append(fields, "cert-pass")
}
case "password":
if connType == "password" || connType == "password-tls" {
if dataMap["username"] == "" {
fields = []string{"username", "password"}
}
@@ -827,19 +805,6 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
return fields
}
// NetworkManager secret flags: bit 1 (value 2) marks a secret as not-saved,
// bit 2 (value 4) as not-required. A not-required secret must not be prompted.
func secretFlagsNotRequired(flags string) bool {
if flags == "" {
return false
}
n, err := strconv.Atoi(flags)
if err != nil {
return false
}
return n&0x4 != 0
}
func needsExternalBrowserAuth(protocol, authType, username string, data map[string]string) bool {
if method, ok := data["saml-auth-method"]; ok {
if method == "REDIRECT" || method == "POST" {
@@ -315,44 +315,6 @@ func TestInferVPNFields_GPSaml(t *testing.T) {
expectedLen: 1,
shouldHave: []string{"password"},
},
{
name: "OpenVPN cert auth (tls) - private-key passphrase",
vpnService: "org.freedesktop.NetworkManager.openvpn",
dataMap: map[string]string{
"connection-type": "tls",
},
expectedLen: 1,
shouldHave: []string{"cert-pass"},
},
{
name: "OpenVPN cert auth (tls) with passphrase-less key",
vpnService: "org.freedesktop.NetworkManager.openvpn",
dataMap: map[string]string{
"connection-type": "tls",
"cert-pass-flags": "4",
},
expectedLen: 0,
},
{
name: "OpenVPN password-tls no username",
vpnService: "org.freedesktop.NetworkManager.openvpn",
dataMap: map[string]string{
"connection-type": "password-tls",
},
expectedLen: 3,
shouldHave: []string{"username", "password", "cert-pass"},
},
{
name: "OpenVPN password-tls with username, passphrase-less key",
vpnService: "org.freedesktop.NetworkManager.openvpn",
dataMap: map[string]string{
"connection-type": "password-tls",
"username": "john",
"cert-pass-flags": "4",
},
expectedLen: 1,
shouldHave: []string{"password"},
},
}
for _, tt := range tests {
+1 -25
View File
@@ -114,14 +114,6 @@ func (b *IWDBackend) discoverDevices() error {
return fmt.Errorf("failed to get managed objects: %w", err)
}
return b.applyManagedObjects(objects)
}
func (b *IWDBackend) applyManagedObjects(objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant) error {
b.stationPath = ""
b.devicePath = ""
b.adapterPath = ""
for path, interfaces := range objects {
if _, hasStation := interfaces[iwdStationInterface]; hasStation {
b.stationPath = path
@@ -137,13 +129,6 @@ func (b *IWDBackend) applyManagedObjects(objects map[dbus.ObjectPath]map[string]
b.stateMutex.Unlock()
}
}
if poweredVar, ok := devProps["Powered"]; ok {
if powered, ok := poweredVar.Value().(bool); ok {
b.stateMutex.Lock()
b.state.WiFiEnabled = powered
b.stateMutex.Unlock()
}
}
}
}
if _, hasAdapter := interfaces[iwdAdapterInterface]; hasAdapter {
@@ -151,18 +136,9 @@ func (b *IWDBackend) applyManagedObjects(objects map[dbus.ObjectPath]map[string]
}
}
if b.devicePath == "" {
if b.stationPath == "" || b.devicePath == "" {
return fmt.Errorf("no WiFi device found")
}
if b.stationPath == "" {
b.stateMutex.Lock()
b.state.WiFiEnabled = false
b.state.WiFiConnected = false
b.state.NetworkStatus = StatusDisconnected
b.state.WiFiNetworks = nil
b.stateMutex.Unlock()
log.Infof("iwd device %s has no station interface; treating WiFi as disabled", b.devicePath)
}
return nil
}
@@ -4,7 +4,6 @@ import (
"fmt"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/godbus/dbus/v5"
)
@@ -36,7 +35,12 @@ func (b *IWDBackend) StartMonitoring(onStateChange func()) error {
}
if b.stationPath != "" {
if err := b.addStationSignalMatch(b.stationPath); err != nil {
err := b.conn.AddMatchSignal(
dbus.WithMatchObjectPath(b.stationPath),
dbus.WithMatchInterface(dbusPropertiesInterface),
dbus.WithMatchMember("PropertiesChanged"),
)
if err != nil {
return fmt.Errorf("failed to add station signal match: %w", err)
}
}
@@ -77,48 +81,6 @@ func (b *IWDBackend) refreshWiFiNetworkState() bool {
return b.updateSavedWiFiNetworks() == nil
}
func (b *IWDBackend) addStationSignalMatch(path dbus.ObjectPath) error {
return b.conn.AddMatchSignal(
dbus.WithMatchObjectPath(path),
dbus.WithMatchInterface(dbusPropertiesInterface),
dbus.WithMatchMember("PropertiesChanged"),
)
}
func (b *IWDBackend) handleStationAdded(path dbus.ObjectPath) bool {
if path == "" || path == b.stationPath {
return false
}
b.stationPath = path
if err := b.addStationSignalMatch(path); err != nil {
log.Warnf("Failed to add iwd station signal match for %s: %v", path, err)
}
if err := b.updateState(); err != nil {
log.Warnf("Failed to update iwd state after station appeared: %v", err)
}
return b.refreshWiFiNetworkState()
}
func (b *IWDBackend) handleStationRemoved(path dbus.ObjectPath) bool {
if path == "" || path != b.stationPath {
return false
}
b.stationPath = ""
b.stateMutex.Lock()
b.state.WiFiEnabled = false
b.state.WiFiConnected = false
b.state.WiFiSSID = ""
b.state.WiFiSignal = 0
b.state.NetworkStatus = StatusDisconnected
b.state.WiFiNetworks = nil
b.stateMutex.Unlock()
return true
}
func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
defer b.sigWG.Done()
@@ -136,13 +98,7 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
if sig.Name == dbusObjectManager+".InterfacesAdded" {
if len(sig.Body) >= 2 {
path, _ := sig.Body[0].(dbus.ObjectPath)
if interfaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant); ok {
if _, ok := interfaces[iwdStationInterface]; ok {
if b.handleStationAdded(path) && b.onStateChange != nil {
b.onStateChange()
}
}
if _, ok := interfaces[iwdKnownNetworkInterface]; ok {
if b.refreshWiFiNetworkState() && b.onStateChange != nil {
b.onStateChange()
@@ -155,15 +111,8 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
if sig.Name == dbusObjectManager+".InterfacesRemoved" {
if len(sig.Body) >= 2 {
path, _ := sig.Body[0].(dbus.ObjectPath)
if interfaces, ok := sig.Body[1].([]string); ok {
for _, iface := range interfaces {
if iface == iwdStationInterface {
if b.handleStationRemoved(path) && b.onStateChange != nil {
b.onStateChange()
}
break
}
if iface == iwdKnownNetworkInterface {
if b.refreshWiFiNetworkState() && b.onStateChange != nil {
b.onStateChange()
@@ -169,41 +169,6 @@ func TestIWDBackend_MapIwdDBusError(t *testing.T) {
}
}
func TestIWDBackend_ApplyManagedObjects_DeviceWithoutStation(t *testing.T) {
backend, _ := NewIWDBackend()
err := backend.applyManagedObjects(map[dbus.ObjectPath]map[string]map[string]dbus.Variant{
"/net/connman/iwd/0/1": {
iwdDeviceInterface: {
"Name": dbus.MakeVariant("wlan0"),
"Powered": dbus.MakeVariant(false),
},
},
})
assert.NoError(t, err)
assert.Equal(t, dbus.ObjectPath("/net/connman/iwd/0/1"), backend.devicePath)
assert.Empty(t, backend.stationPath)
state, err := backend.GetCurrentState()
assert.NoError(t, err)
assert.Equal(t, "wlan0", state.WiFiDevice)
assert.False(t, state.WiFiEnabled)
assert.False(t, state.WiFiConnected)
}
func TestIWDBackend_ApplyManagedObjects_NoDevice(t *testing.T) {
backend, _ := NewIWDBackend()
err := backend.applyManagedObjects(map[dbus.ObjectPath]map[string]map[string]dbus.Variant{
"/net/connman/iwd/0": {
iwdAdapterInterface: {},
},
})
assert.ErrorContains(t, err, "no WiFi device found")
}
func TestIWDSavedWiFiProfilesFromManagedObjects(t *testing.T) {
objects := map[dbus.ObjectPath]map[string]map[string]dbus.Variant{
"/net/connman/iwd/known_network/1": {
@@ -90,10 +90,7 @@ type pendingVPNCredentials struct {
ConnectionPath string
Username string
Password string
// Secrets holds all VPN secret fields keyed by name (e.g. "cert-pass");
// falls back to Password under the "password" key when empty.
Secrets map[string]string
SavePassword bool
SavePassword bool
}
type cachedVPNCredentials struct {
@@ -863,17 +863,13 @@ func (b *NetworkManagerBackend) saveVPNCredentials(creds *pendingVPNCredentials)
log.Infof("[saveVPNCredentials] Saving username")
}
// Save secrets if requested
// Save password if requested
if creds.SavePassword {
secs := creds.Secrets
if len(secs) == 0 {
secs = map[string]string{"password": creds.Password}
}
for field := range secs {
data[field+"-flags"] = "0"
}
data["password-flags"] = "0"
secs := make(map[string]string)
secs["password"] = creds.Password
vpn["secrets"] = dbus.MakeVariant(secs)
log.Infof("[saveVPNCredentials] Saving %d secret field(s) with flags=0", len(secs))
log.Infof("[saveVPNCredentials] Saving password with password-flags=0")
}
vpn["data"] = dbus.MakeVariant(data)
@@ -42,15 +42,12 @@ func HandleListInstalled(conn net.Conn, req models.Request) {
for _, id := range installedNames {
if plugin, ok := pluginMap[id]; ok {
hasUpdate := false
diffURL := plugin.Repo
if hasUpdates, dURL, err := manager.HasUpdates(id, plugin); err == nil {
if hasUpdates, err := manager.HasUpdates(id, plugin); err == nil {
hasUpdate = hasUpdates
diffURL = dURL
}
info := pluginInfoFromPlugin(plugin)
info.HasUpdate = hasUpdate
info.DiffURL = diffURL
result = append(result, info)
} else {
result = append(result, PluginInfo{
-1
View File
@@ -18,7 +18,6 @@ type PluginInfo struct {
Note string `json:"note,omitempty"`
HasUpdate bool `json:"hasUpdate,omitempty"`
RequiresDMS string `json:"requires_dms,omitempty"`
DiffURL string `json:"diffUrl,omitempty"`
Upvotes int `json:"upvotes,omitempty"`
Status []string `json:"status,omitempty"`
IssueURL string `json:"issueUrl,omitempty"`
-2
View File
@@ -17,7 +17,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
@@ -191,7 +190,6 @@ func InitializeFreedeskManager() error {
}
freedesktopManager = manager
matugen.SetColorSchemeEchoHook(manager.ExpectColorSchemeEcho)
log.Info("Freedesktop manager initialized")
return nil
+13 -62
View File
@@ -16,10 +16,6 @@ type activeSchedule struct {
nextFire time.Time
}
// Grace period before a missed-while-off schedule fires, so a freshly
// connected frontend has its cycle-seq baseline before the bump arrives.
var catchUpDelay = 3 * time.Second
type Manager struct {
config Config
configMutex sync.RWMutex
@@ -33,20 +29,9 @@ type Manager struct {
updateTrigger chan struct{}
resetReq chan string
wg sync.WaitGroup
// Owned by schedulerLoop after start; keyed like schedules ("" = global).
lastFires map[string]time.Time
persistFires func(map[string]time.Time)
}
func NewManager() *Manager {
return newManager(loadLastFires(), saveLastFires)
}
func newManager(lastFires map[string]time.Time, persistFires func(map[string]time.Time)) *Manager {
if lastFires == nil {
lastFires = map[string]time.Time{}
}
m := &Manager{
config: Config{
Global: ScheduleConfig{Mode: "interval", IntervalSec: 300, Time: "06:00"},
@@ -55,8 +40,6 @@ func newManager(lastFires map[string]time.Time, persistFires func(map[string]tim
stopChan: make(chan struct{}),
updateTrigger: make(chan struct{}, 1),
resetReq: make(chan string, 8),
lastFires: lastFires,
persistFires: persistFires,
}
m.state = &State{Config: m.getConfig()}
@@ -132,7 +115,9 @@ func (m *Manager) Close() {
func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
ch := lm.Subscribe("wallpaper")
m.wg.Go(func() {
m.wg.Add(1)
go func() {
defer m.wg.Done()
defer lm.Unsubscribe("wallpaper")
for {
select {
@@ -148,7 +133,7 @@ func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
m.TriggerUpdate()
}
}
})
}()
}
func (m *Manager) schedulerLoop() {
@@ -169,26 +154,11 @@ func (m *Manager) schedulerLoop() {
delete(schedules, key)
}
}
firesDirty := false
for key, cfg := range active {
s, ok := schedules[key]
switch {
case !ok:
s = &activeSchedule{cfg: cfg, nextFire: computeNext(now, cfg)}
schedules[key] = s
if cfg.Mode == "time" {
last := m.lastFires[key]
prev, valid := prevDailyTime(now, cfg.Time)
switch {
case last.IsZero():
// First enable (or no history): seed instead of firing.
m.lastFires[key] = now
firesDirty = true
case valid && last.Before(prev):
// Scheduled time passed while dms wasn't running.
s.nextFire = now.Add(catchUpDelay)
}
}
schedules[key] = &activeSchedule{cfg: cfg, nextFire: computeNext(now, cfg)}
case s.cfg != cfg || resets[key]:
s.cfg = cfg
s.nextFire = computeNext(now, cfg)
@@ -201,22 +171,9 @@ func (m *Manager) schedulerLoop() {
if !s.nextFire.After(now) {
dueKeys = append(dueKeys, key)
s.nextFire = computeNext(now, s.cfg)
if s.cfg.Mode == "time" {
m.lastFires[key] = now
firesDirty = true
}
}
}
if firesDirty {
for key := range m.lastFires {
if _, ok := schedules[key]; !ok {
delete(m.lastFires, key)
}
}
m.persistFires(m.lastFires)
}
next, hasNext := soonest(schedules)
if len(dueKeys) == 0 {
m.setState(config, next, seq, "")
@@ -228,7 +185,10 @@ func (m *Manager) schedulerLoop() {
waitDur := 24 * time.Hour
if hasNext {
waitDur = max(time.Until(next), time.Second)
waitDur = time.Until(next)
if waitDur < time.Second {
waitDur = time.Second
}
}
if timer != nil {
@@ -302,7 +262,10 @@ func computeNext(now time.Time, cfg ScheduleConfig) time.Time {
case "time":
return nextDailyTime(now, cfg.Time)
default:
sec := max(cfg.IntervalSec, 1)
sec := cfg.IntervalSec
if sec < 1 {
sec = 1
}
return now.Add(time.Duration(sec) * time.Second)
}
}
@@ -319,18 +282,6 @@ func nextDailyTime(now time.Time, hhmm string) time.Time {
return next
}
func prevDailyTime(now time.Time, hhmm string) (time.Time, bool) {
hour, minute, ok := parseHHMM(hhmm)
if !ok {
return time.Time{}, false
}
prev := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location())
if prev.After(now) {
prev = prev.Add(-24 * time.Hour)
}
return prev, true
}
func parseHHMM(hhmm string) (int, int, bool) {
parts := strings.Split(hhmm, ":")
if len(parts) != 2 {
+1 -74
View File
@@ -83,81 +83,8 @@ func TestActiveSchedules(t *testing.T) {
}
}
func TestPrevDailyTime(t *testing.T) {
now := time.Date(2026, 6, 30, 12, 0, 0, 0, time.UTC)
earlier, ok := prevDailyTime(now, "06:00")
if !ok || !earlier.Equal(time.Date(2026, 6, 30, 6, 0, 0, 0, time.UTC)) {
t.Errorf("prev (today) = %v, %v", earlier, ok)
}
later, ok := prevDailyTime(now, "18:00")
if !ok || !later.Equal(time.Date(2026, 6, 29, 18, 0, 0, 0, time.UTC)) {
t.Errorf("prev (yesterday) = %v, %v", later, ok)
}
if _, ok := prevDailyTime(now, "bad"); ok {
t.Error("prev (invalid) should not be ok")
}
}
func TestTimeScheduleCatchUpAfterRestart(t *testing.T) {
origDelay := catchUpDelay
catchUpDelay = 100 * time.Millisecond
defer func() { catchUpDelay = origDelay }()
now := time.Now()
hhmm := now.Add(-2 * time.Hour).Format("15:04")
seed := map[string]time.Time{"": now.Add(-26 * time.Hour)}
m := newManager(seed, func(map[string]time.Time) {})
defer m.Close()
sub := m.Subscribe("test")
defer m.Unsubscribe("test")
m.SetConfig(Config{Global: ScheduleConfig{Enabled: true, Mode: "time", Time: hhmm}})
deadline := time.After(3 * time.Second)
for {
select {
case state := <-sub:
if state.CycleSeq > 0 && state.Target == "" {
return
}
case <-deadline:
t.Fatal("missed time schedule did not catch up within 3s")
}
}
}
func TestTimeScheduleNoFireOnFirstEnable(t *testing.T) {
now := time.Now()
hhmm := now.Add(-2 * time.Hour).Format("15:04")
m := newManager(map[string]time.Time{}, func(map[string]time.Time) {})
defer m.Close()
sub := m.Subscribe("test")
defer m.Unsubscribe("test")
m.SetConfig(Config{Global: ScheduleConfig{Enabled: true, Mode: "time", Time: hhmm}})
deadline := time.After(1500 * time.Millisecond)
for {
select {
case state := <-sub:
if state.CycleSeq > 0 {
t.Fatal("first enable must not fire immediately")
}
case <-deadline:
return
}
}
}
func TestSchedulerEmitsCycle(t *testing.T) {
m := newManager(map[string]time.Time{}, func(map[string]time.Time) {})
m := NewManager()
defer m.Close()
sub := m.Subscribe("test")
-45
View File
@@ -1,45 +0,0 @@
package wallpaper
import (
"encoding/json"
"os"
"path/filepath"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
type persistentState struct {
LastFires map[string]time.Time `json:"lastFires"`
}
func stateFilePath() string {
return filepath.Join(utils.XDGCacheHome(), "dms", "wallpaper-schedule.json")
}
func loadLastFires() map[string]time.Time {
data, err := os.ReadFile(stateFilePath())
if err != nil {
return map[string]time.Time{}
}
var state persistentState
if err := json.Unmarshal(data, &state); err != nil || state.LastFires == nil {
return map[string]time.Time{}
}
return state.LastFires
}
func saveLastFires(fires map[string]time.Time) {
path := stateFilePath()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return
}
data, err := json.Marshal(persistentState{LastFires: fires})
if err != nil {
return
}
if err := os.WriteFile(path, data, 0o644); err != nil {
log.Warnf("wallpaper: failed to persist schedule state: %v", err)
}
}
+4 -10
View File
@@ -108,7 +108,10 @@ func (sc *SharedContext) eventDispatcher() {
if r := recover(); r != nil {
err := fmt.Errorf("FATAL: Wayland event dispatcher panic: %v", r)
log.Error(err)
sc.signalFatal(err)
select {
case sc.fatalError <- err:
default:
}
}
}()
@@ -138,7 +141,6 @@ func (sc *SharedContext) eventDispatcher() {
continue
case err != nil:
log.Errorf("Poll error: %v", err)
sc.signalFatal(fmt.Errorf("wayland poll error: %w", err))
return
}
@@ -159,7 +161,6 @@ func (sc *SharedContext) eventDispatcher() {
if consecutiveErrors >= maxConsecutiveErrors {
log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors)
sc.signalFatal(fmt.Errorf("wayland connection unrecoverable after %d dispatch failures: %w", maxConsecutiveErrors, err))
return
}
@@ -171,13 +172,6 @@ func (sc *SharedContext) eventDispatcher() {
}
}
func (sc *SharedContext) signalFatal(err error) {
select {
case sc.fatalError <- err:
default:
}
}
func (sc *SharedContext) drainCmdQueue() {
for {
select {
+1 -51
View File
@@ -9,7 +9,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
tea "github.com/charmbracelet/bubbletea"
)
@@ -130,7 +129,7 @@ func (m Model) deployConfigurations() tea.Cmd {
deployer := config.NewConfigDeployer(m.logChan)
results, err := deployer.DeployConfigurationsSelectiveWithReinstallsAndSystemd(context.Background(), wm, terminal, m.dependencies, m.replaceConfigs, m.reinstallItems, m.useSystemdConfig())
results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(context.Background(), wm, terminal, m.dependencies, m.replaceConfigs, m.reinstallItems)
return configDeploymentResult{
results: results,
@@ -139,17 +138,6 @@ func (m Model) deployConfigurations() tea.Cmd {
}
}
func (m Model) useSystemdConfig() bool {
if m.osInfo == nil {
return true
}
distroConfig, exists := distros.Registry[m.osInfo.Distribution.ID]
if !exists {
return true
}
return distroConfig.Family != distros.FamilyVoid
}
func (m Model) viewConfigConfirmation() string {
var b strings.Builder
@@ -207,50 +195,12 @@ func (m Model) viewConfigConfirmation() string {
b.WriteString(backup)
b.WriteString("\n\n")
if note := m.configReplacementNote(); note != "" {
b.WriteString(m.styles.Subtle.Render(note))
b.WriteString("\n\n")
}
help := m.styles.Subtle.Render("↑/↓: Navigate, Space: Toggle replace/keep, Enter: Continue")
b.WriteString(help)
return b.String()
}
func (m Model) configReplacementNote() string {
if m.selectedConfig < 0 || m.selectedConfig >= len(m.existingConfigs) {
return ""
}
configInfo := m.existingConfigs[m.selectedConfig]
if !configInfo.Exists {
return ""
}
switch configInfo.ConfigType {
case "Niri":
if m.useSystemdConfig() {
return "Replacing Niri writes the DMS Niri template and uses the user systemd dms service for shell autostart."
}
return `Replacing Niri writes the DMS Niri template and starts DMS from Niri with spawn-at-startup "dms" "run".`
case "Hyprland":
if m.useSystemdConfig() {
return "Replacing Hyprland writes the DMS Lua template and uses the user systemd dms service for shell autostart."
}
return `Replacing Hyprland writes the DMS Lua template and starts DMS from Hyprland with hl.exec_cmd("dms run").`
case "Mango":
return "Replacing Mango writes the DMS Mango template and starts DMS from Mango with exec-once=dms run."
case "Ghostty":
return "Replacing Ghostty writes the DMS terminal defaults and theme include."
case "Kitty":
return "Replacing Kitty writes the DMS terminal defaults, theme include, and tab styling."
case "Alacritty":
return "Replacing Alacritty writes the DMS terminal defaults and theme import."
default:
return ""
}
}
func (m Model) updateConfigConfirmationState(msg tea.Msg) (tea.Model, tea.Cmd) {
if result, ok := msg.(configCheckResult); ok {
if result.error != nil {
+10 -33
View File
@@ -140,7 +140,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
return "dms-shell-git"
}
return "dms-shell"
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE, distros.FamilyVoid:
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
if isGit {
return "dms-git"
}
@@ -168,8 +168,6 @@ func uninstallCommand(distroID string, dependencies []deps.Dependency) string {
return "sudo apt remove " + pkg
case distros.FamilySUSE:
return "sudo zypper remove " + pkg
case distros.FamilyVoid:
return "sudo xbps-remove -R " + pkg
default:
return ""
}
@@ -203,26 +201,15 @@ func (m Model) viewInstallComplete() string {
wm := m.selectedWindowManager()
// mango launches DMS via `exec-once=dms run` (not a systemd session target)
loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\""
if !m.useSystemdConfig() {
switch wm {
case deps.WindowManagerNiri:
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session niri"
case deps.WindowManagerHyprland:
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session Hyprland"
case deps.WindowManagerMango:
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session mango"
}
} else {
// mango launches DMS via `exec-once=dms run` (not a systemd session target)
switch wm {
case deps.WindowManagerNiri:
loginHint = "If you do not have a greeter, login with \"niri-session\""
case deps.WindowManagerHyprland:
loginHint = "If you do not have a greeter, login with \"Hyprland\""
case deps.WindowManagerMango:
loginHint = "If you do not have a greeter, login with \"mango\""
}
switch wm {
case deps.WindowManagerNiri:
loginHint = "If you do not have a greeter, login with \"niri-session\""
case deps.WindowManagerHyprland:
loginHint = "If you do not have a greeter, login with \"Hyprland\""
case deps.WindowManagerMango:
loginHint = "If you do not have a greeter, login with \"mango\""
}
b.WriteString("\n")
@@ -235,17 +222,7 @@ func (m Model) viewInstallComplete() string {
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle))
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
if !m.useSystemdConfig() {
switch wm {
case deps.WindowManagerNiri:
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render(`remove spawn-at-startup "dms" "run" from ~/.config/niri/config.kdl`) + "\n")
case deps.WindowManagerHyprland:
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render(`remove hl.exec_cmd("dms run") from ~/.config/hypr/hyprland.lua`) + "\n")
case deps.WindowManagerMango:
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n")
}
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("quickshell --path ~/.config/quickshell/dms log") + "\n")
} else if wm == deps.WindowManagerMango {
if wm == deps.WindowManagerMango {
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n")
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n")
} else {
+1 -72
View File
@@ -1,10 +1,7 @@
package utils
import (
"os"
"os/exec"
"path/filepath"
"strings"
)
type AppChecker interface {
@@ -34,20 +31,7 @@ func (DefaultAppChecker) AnyFlatpakExists(flatpaks ...string) bool {
func CommandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
if err == nil {
return true
}
if strings.ContainsRune(cmd, os.PathSeparator) {
return false
}
for _, dir := range userBinDirs() {
path := filepath.Join(dir, cmd)
info, statErr := os.Stat(path)
if statErr == nil && !info.IsDir() && info.Mode()&0o111 != 0 {
return true
}
}
return false
return err == nil
}
func AnyCommandExists(cmds ...string) bool {
@@ -58,58 +42,3 @@ func AnyCommandExists(cmds ...string) bool {
}
return false
}
func EnvWithUserBinPath(env []string) []string {
if env == nil {
env = os.Environ()
}
out := append([]string(nil), env...)
pathIndex := -1
pathValue := ""
for i, entry := range out {
if strings.HasPrefix(entry, "PATH=") {
pathIndex = i
pathValue = strings.TrimPrefix(entry, "PATH=")
break
}
}
parts := filepath.SplitList(pathValue)
seen := make(map[string]struct{}, len(parts))
for _, part := range parts {
if part != "" {
seen[part] = struct{}{}
}
}
prepend := make([]string, 0, 2)
for _, dir := range userBinDirs() {
if dir == "" {
continue
}
if _, ok := seen[dir]; ok {
continue
}
prepend = append(prepend, dir)
seen[dir] = struct{}{}
}
parts = append(prepend, parts...)
newPath := "PATH=" + strings.Join(parts, string(os.PathListSeparator))
if pathIndex >= 0 {
out[pathIndex] = newPath
} else {
out = append(out, newPath)
}
return out
}
func userBinDirs() []string {
dirs := []string{}
if home, err := os.UserHomeDir(); err == nil && home != "" {
dirs = append(dirs, filepath.Join(home, ".local", "bin"))
}
dirs = append(dirs, "/usr/local/bin")
return dirs
}
-55
View File
@@ -1,55 +0,0 @@
package utils
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCommandExistsFallsBackToLocalBin(t *testing.T) {
home := t.TempDir()
binDir := filepath.Join(home, ".local", "bin")
require.NoError(t, os.MkdirAll(binDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(binDir, "pywalfox"), []byte("#!/bin/sh\n"), 0o755))
t.Setenv("HOME", home)
t.Setenv("PATH", t.TempDir())
assert.True(t, CommandExists("pywalfox"))
}
func TestCommandExistsIgnoresNonExecutableLocalBinFile(t *testing.T) {
home := t.TempDir()
binDir := filepath.Join(home, ".local", "bin")
require.NoError(t, os.MkdirAll(binDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(binDir, "pywalfox"), []byte("not executable"), 0o644))
t.Setenv("HOME", home)
t.Setenv("PATH", t.TempDir())
assert.False(t, CommandExists("pywalfox"))
}
func TestEnvWithUserBinPathPrependsLocalBin(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
env := EnvWithUserBinPath([]string{"PATH=/usr/bin", "OTHER=value"})
var pathValue string
for _, entry := range env {
if strings.HasPrefix(entry, "PATH=") {
pathValue = strings.TrimPrefix(entry, "PATH=")
break
}
}
parts := filepath.SplitList(pathValue)
require.NotEmpty(t, parts)
assert.Equal(t, filepath.Join(home, ".local", "bin"), parts[0])
assert.Contains(t, parts, "/usr/local/bin")
assert.Contains(t, env, "OTHER=value")
}
-1
View File
@@ -74,7 +74,6 @@ override_dh_auto_install:
install -Dm644 assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \
install -Dm644 assets/dms-open.desktop debian/dms-git/usr/share/applications/dms-open.desktop; \
install -Dm644 assets/com.danklinux.dms.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.desktop; \
install -Dm644 assets/com.danklinux.dms.notepad.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.notepad.desktop; \
install -Dm644 assets/danklogo.svg debian/dms-git/usr/share/icons/hicolor/scalable/apps/danklogo.svg; \
else \
echo "ERROR: quickshell directory not found!" && \
-1
View File
@@ -67,7 +67,6 @@ override_dh_auto_install:
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/systemd/dms.service debian/dms/usr/lib/systemd/user/dms.service; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/dms-open.desktop debian/dms/usr/share/applications/dms-open.desktop; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.desktop debian/dms/usr/share/applications/com.danklinux.dms.desktop; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.notepad.desktop debian/dms/usr/share/applications/com.danklinux.dms.notepad.desktop; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/danklogo.svg debian/dms/usr/share/icons/hicolor/scalable/apps/danklogo.svg; \
else \
echo "ERROR: DankMaterialShell-$(UPSTREAM_VERSION) directory not found!" && \
-2
View File
@@ -122,7 +122,6 @@ install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.servic
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
install -Dm644 assets/com.danklinux.dms.notepad.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.notepad.desktop
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
# Install shell files to shared data location
@@ -147,7 +146,6 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%{_userunitdir}/dms.service
%{_datadir}/applications/dms-open.desktop
%{_datadir}/applications/com.danklinux.dms.desktop
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
%files -n dms-cli
-2
View File
@@ -90,7 +90,6 @@ install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_us
install -Dm644 %{_builddir}/dms-qml/assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 %{_builddir}/dms-qml/assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
install -Dm644 %{_builddir}/dms-qml/assets/com.danklinux.dms.notepad.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.notepad.desktop
install -Dm644 %{_builddir}/dms-qml/assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
@@ -114,7 +113,6 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%{_userunitdir}/dms.service
%{_datadir}/applications/dms-open.desktop
%{_datadir}/applications/com.danklinux.dms.desktop
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
%files -n dms-cli
+2 -1
View File
@@ -18,7 +18,8 @@ in
]
++ lib.optional cfg.enableDynamicTheming pkgs.matugen
++ lib.optional cfg.enableAudioWavelength pkgs.cava
++ lib.optional cfg.enableCalendarEvents pkgs.khal;
++ lib.optional cfg.enableCalendarEvents pkgs.khal
++ lib.optional cfg.enableClipboardPaste pkgs.wtype;
plugins = lib.mapAttrs (name: plugin: {
source = plugin.src;
-1
View File
@@ -28,7 +28,6 @@ let
cfg.quickshell.package
compositorPackage
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
pkgs.jq # reads the user's cursor theme from settings.json in dms-greeter
]
}
${
+1 -1
View File
@@ -93,7 +93,7 @@ in {
text = lib.pipe cfg'.filesToInclude [
(map (filename: "dms/${filename}"))
withOriginalConfig
(map (filename: "include optional=true \"${filename}.kdl\""))
(map (filename: "include \"${filename}.kdl\""))
(files: files ++ fixes)
(builtins.concatStringsSep "\n")
];
+1 -1
View File
@@ -76,7 +76,7 @@ in
enableClipboardPaste = lib.mkOption {
type = types.bool;
default = true;
description = "Deprecated: paste is built into dms; no extra dependencies needed. Kept as a no-op for compatibility.";
description = "Adds needed dependencies for directly pasting items from the clipboard history.";
};
quickshell = {
+2 -2
View File
@@ -6,8 +6,8 @@
let
homeManagerNixosModule =
(fetchTarball {
url = "https://github.com/nix-community/home-manager/archive/53ebbdc405acc04acd9bb73ccca462b51ddb8c6d.tar.gz";
sha256 = "1cqmfgwb3jac2zzv82bwvgypxff1z30xkz9j6qcinkmqf58j3k3b";
url = "https://github.com/nix-community/home-manager/archive/e82d4a4ecd18363aa2054cbaa3e32e4134c3dbf4.tar.gz";
sha256 = "sha256-ZTYDofOM3/PJhRF1EuBh6uibm+DmkhU7Wor6mMN7YTc=";
})
+ "/nixos";
in
+4 -4
View File
@@ -6,8 +6,8 @@
let
homeManagerNixosModule =
(fetchTarball {
url = "https://github.com/nix-community/home-manager/archive/53ebbdc405acc04acd9bb73ccca462b51ddb8c6d.tar.gz";
sha256 = "1cqmfgwb3jac2zzv82bwvgypxff1z30xkz9j6qcinkmqf58j3k3b";
url = "https://github.com/nix-community/home-manager/archive/e82d4a4ecd18363aa2054cbaa3e32e4134c3dbf4.tar.gz";
sha256 = "sha256-ZTYDofOM3/PJhRF1EuBh6uibm+DmkhU7Wor6mMN7YTc=";
})
+ "/nixos";
@@ -76,8 +76,8 @@ pkgs.testers.runNixOSTest {
machine.wait_for_unit("multi-user.target")
machine.succeed("su -- danklinux -c 'test -f ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"include optional=true \\\"dms/binds.kdl\\\"\" ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"include optional=true \\\"hm.kdl\\\"\" ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"include \\\"dms/binds.kdl\\\"\" ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"include \\\"hm.kdl\\\"\" ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"spawn-at-startup\" ~/.config/niri/hm.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"\\\"dms\\\" \\\"run\\\"\" ~/.config/niri/hm.kdl'")
'';
+1 -3
View File
@@ -25,7 +25,7 @@ Recommends: danksearch
Recommends: matugen
Recommends: quickshell-git
Recommends: NetworkManager
Recommends: qt6-multimedia-imports
Recommends: qt6-qtmultimedia
Suggests: cups-pk-helper
Suggests: qt6ct
@@ -117,7 +117,6 @@ install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.servic
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
install -Dm644 assets/com.danklinux.dms.notepad.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.notepad.desktop
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
@@ -154,7 +153,6 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%{_userunitdir}/dms.service
%{_datadir}/applications/dms-open.desktop
%{_datadir}/applications/com.danklinux.dms.desktop
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
%dir %{_datadir}/icons/hicolor
%dir %{_datadir}/icons/hicolor/scalable
%dir %{_datadir}/icons/hicolor/scalable/apps
+1 -3
View File
@@ -26,7 +26,7 @@ Recommends: cava
Recommends: danksearch
Recommends: matugen
Recommends: NetworkManager
Recommends: qt6-multimedia-imports
Recommends: qt6-qtmultimedia
Suggests: cups-pk-helper
Suggests: qt6ct
@@ -66,7 +66,6 @@ install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.servic
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
install -Dm644 assets/com.danklinux.dms.notepad.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.notepad.desktop
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
@@ -101,7 +100,6 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%{_userunitdir}/dms.service
%{_datadir}/applications/dms-open.desktop
%{_datadir}/applications/com.danklinux.dms.desktop
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
%dir %{_datadir}/icons/hicolor
%dir %{_datadir}/icons/hicolor/scalable
%dir %{_datadir}/icons/hicolor/scalable/apps
-2
View File
@@ -75,8 +75,6 @@ override_dh_auto_install:
debian/dms-git/usr/share/applications/dms-open.desktop
install -Dm644 dms-git-repo/assets/com.danklinux.dms.desktop \
debian/dms-git/usr/share/applications/com.danklinux.dms.desktop
install -Dm644 dms-git-repo/assets/com.danklinux.dms.notepad.desktop \
debian/dms-git/usr/share/applications/com.danklinux.dms.notepad.desktop
install -Dm644 dms-git-repo/assets/danklogo.svg \
debian/dms-git/usr/share/icons/hicolor/scalable/apps/danklogo.svg
-2
View File
@@ -56,8 +56,6 @@ override_dh_auto_install:
debian/dms/usr/share/applications/dms-open.desktop
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/com.danklinux.dms.desktop \
debian/dms/usr/share/applications/com.danklinux.dms.desktop
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/com.danklinux.dms.notepad.desktop \
debian/dms/usr/share/applications/com.danklinux.dms.notepad.desktop
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/danklogo.svg \
debian/dms/usr/share/icons/hicolor/scalable/apps/danklogo.svg
+3 -23
View File
@@ -52,8 +52,8 @@ checkout at `srcpkgs/<pkg>/template` to build or submit it.
## Dependencies
Installing `dms` automatically pulls in `quickshell`, `accountsservice`, `dgop`,
`matugen` (which drives the Material You theming), `dbus`, and `elogind`.
The rest are optional, install whichever features you want:
and `matugen` (which drives the Material You theming). The rest are optional —
install whichever features you want:
| Package | Enables |
| --- | --- |
@@ -61,6 +61,7 @@ The rest are optional, install whichever features you want:
| `cava` | audio visualiser widget |
| `qt6-multimedia` | system sound feedback |
| `qt6ct` | Qt app theming |
| `wtype` | virtual keyboard input |
| `power-profiles-daemon` | power profile control |
| `cups-pk-helper` | printer management |
| `NetworkManager` | network control |
@@ -98,27 +99,6 @@ spawn-at-startup "dms" "run"
or Hyprland: `exec-once = dms run`.
From a TTY on Void without a greeter, start your compositor through a D-Bus
session:
```sh
dbus-run-session niri
dbus-run-session Hyprland
dbus-run-session mango
```
The `mangowc` package provides the `mango` command.
For power menu actions to work on runit systems, make sure the system D-Bus and
elogind services are enabled:
```sh
sudo ln -sf /etc/sv/dbus /var/service/dbus
sudo ln -sf /etc/sv/elogind /var/service/elogind
```
The `dankinstall` Void path does this automatically after installing packages.
## Greeter (optional)
Install `dms-greeter`, then let the CLI do the setup:
+2 -2
View File
@@ -32,17 +32,17 @@ conflicts="dms"
provides="dms-${version}_${revision}"
# Optional feature deps are listed in distro/void/README.md.
depends="quickshell accountsservice dgop matugen dbus elogind"
depends="quickshell accountsservice dgop matugen dbus"
post_install() {
# QML shell tree (build_style=go already installed the dms binary)
vmkdir usr/share/quickshell/dms
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION"
# Desktop entry + icon
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
vinstall "${wrksrc}/assets/com.danklinux.dms.desktop" 644 usr/share/applications
vinstall "${wrksrc}/assets/com.danklinux.dms.notepad.desktop" 644 usr/share/applications
vinstall "${wrksrc}/assets/danklogo.svg" 644 usr/share/icons/hicolor/scalable/apps
# Shell completions (generated by the built binary; skip when cross-building)
+2 -2
View File
@@ -22,17 +22,17 @@ distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v$
checksum=f54601e522c883fa9cce02bec070e4321e47389a1cf453e7ad0bb7379ad91b61
# Optional feature deps are listed in distro/void/README.md.
depends="quickshell accountsservice dgop matugen dbus elogind"
depends="quickshell accountsservice dgop matugen dbus"
post_install() {
# QML shell tree (build_style=go already installed the dms binary)
vmkdir usr/share/quickshell/dms
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION"
# Desktop entry + icon
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
vinstall "${wrksrc}/assets/com.danklinux.dms.desktop" 644 usr/share/applications
vinstall "${wrksrc}/assets/com.danklinux.dms.notepad.desktop" 644 usr/share/applications
vinstall "${wrksrc}/assets/danklogo.svg" 644 usr/share/icons/hicolor/scalable/apps
# Shell completions (generated by the built binary; skip when cross-building)
-4
View File
@@ -684,10 +684,6 @@ Notepad/scratchpad modal control for quick note-taking.
- `open` - Show notepad modal
- `close` - Hide notepad modal
- `toggle` - Toggle notepad modal visibility
- `openFile <path>` - Show notepad and load a file into a tab, reusing an existing tab if that file is already open
- Parameters: `path` - Absolute path to a file. Empty path behaves like `open`.
- Returns: Success/failure message
- This is what `com.danklinux.dms.notepad.desktop`'s `Exec=dms ipc call notepad openFile %f` uses so Notepad shows up in "Open With" pickers for text files.
- `expand` - Expand the active notepad width and open it if hidden
- `collapse` - Collapse the active notepad width without changing visibility
- `toggleExpand` - Toggle the active notepad width between collapsed and expanded
Generated
+3 -3
View File
@@ -18,11 +18,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1783224372,
"narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=",
"lastModified": 1778443072,
"narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d407951447dcd00442e97087bf374aad70c04cea",
"rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
"type": "github"
},
"original": {
-2
View File
@@ -133,8 +133,6 @@
$out/share/applications/dms-open.desktop
install -D ${rootSrc}/assets/com.danklinux.dms.desktop \
$out/share/applications/com.danklinux.dms.desktop
install -D ${rootSrc}/assets/com.danklinux.dms.notepad.desktop \
$out/share/applications/com.danklinux.dms.notepad.desktop
install -D ${rootSrc}/core/assets/danklogo.svg \
$out/share/hicolor/scalable/apps/danklogo.svg
+27 -79
View File
@@ -68,6 +68,27 @@ const KEY_MAP = {
16777429: "XF86Explorer",
16777442: "XF86Launch0",
16777443: "XF86Launch1",
33: "1",
64: "2",
35: "3",
36: "4",
37: "5",
94: "6",
38: "7",
42: "8",
40: "9",
41: "0",
60: "Comma",
62: "Period",
63: "Slash",
58: "Semicolon",
34: "Apostrophe",
123: "BracketLeft",
125: "BracketRight",
124: "Backslash",
95: "Minus",
43: "Equal",
126: "grave",
196: "Adiaeresis",
214: "Odiaeresis",
220: "Udiaeresis",
@@ -105,56 +126,6 @@ const KEY_MAP = {
161: "exclamdown"
};
// Preserve unshifted symbols from the active layout
const SYMBOL_KEYSYM = {
33: "exclam",
34: "quotedbl",
35: "numbersign",
36: "dollar",
37: "percent",
38: "ampersand",
40: "parenleft",
41: "parenright",
42: "asterisk",
43: "plus",
58: "colon",
60: "less",
62: "greater",
63: "question",
64: "at",
94: "asciicircum",
95: "underscore",
123: "braceleft",
124: "bar",
125: "braceright",
126: "asciitilde"
};
// Preserve the existing shifted-US physical-key mapping
const SHIFTED_US_FALLBACK = {
33: "1",
34: "Apostrophe",
35: "3",
36: "4",
37: "5",
38: "7",
40: "9",
41: "0",
42: "8",
43: "Equal",
58: "Semicolon",
60: "Comma",
62: "Period",
63: "Slash",
64: "2",
94: "6",
95: "Minus",
123: "BracketLeft",
124: "Backslash",
125: "BracketRight",
126: "grave"
};
// Numpad (keypad) keys. Qt reuses the same Qt::Key_* values for the numpad and
// the main rows/nav cluster; only Qt.KeypadModifier distinguishes them. niri and
// the other compositors bind against the xkb KP_* keysym names, so we must emit
@@ -182,17 +153,13 @@ const KP_MAP = {
46: "KP_Decimal"
};
function xkbKeyFromQtKey(qk, isKeypad, hasShift) {
function xkbKeyFromQtKey(qk, isKeypad) {
if (isKeypad) {
if (qk >= 48 && qk <= 57)
return "KP_" + (qk - 48);
if (KP_MAP[qk])
return KP_MAP[qk];
}
if (!hasShift && SYMBOL_KEYSYM[qk])
return SYMBOL_KEYSYM[qk];
if (hasShift && SHIFTED_US_FALLBACK[qk])
return SHIFTED_US_FALLBACK[qk];
if (qk >= 65 && qk <= 90)
return String.fromCharCode(qk);
if (qk >= 97 && qk <= 122)
@@ -225,42 +192,23 @@ function formatToken(mods, key) {
return (mods.length ? mods.join("+") + "+" : "") + key;
}
function canonicalModifier(modifier) {
var normalized = (modifier || "").toLowerCase();
if (normalized === "control")
return "ctrl";
if (normalized === "win")
return "super";
return normalized;
}
function withSymbolicMod(mods, modKey) {
var configuredMod = canonicalModifier(modKey);
if (!configuredMod)
return mods;
return mods.map(function (modifier) {
return canonicalModifier(modifier) === configuredMod ? "Mod" : modifier;
});
}
function normalizeKeyCombo(keyCombo, modKey) {
function normalizeKeyCombo(keyCombo) {
if (!keyCombo)
return "";
var configuredMod = canonicalModifier(modKey) || "super";
return keyCombo.toLowerCase().replace(/\bmod\b/g, configuredMod).replace(/\bcontrol\b/g, "ctrl").replace(/\bwin\b/g, "super");
return keyCombo.toLowerCase().replace(/\bmod\b/g, "super").replace(/\bsuper\b/g, "super");
}
function getConflictingBinds(keyCombo, currentAction, allBinds, modKey) {
function getConflictingBinds(keyCombo, currentAction, allBinds) {
if (!keyCombo)
return [];
var conflicts = [];
var normalizedKey = normalizeKeyCombo(keyCombo, modKey);
var normalizedKey = normalizeKeyCombo(keyCombo);
for (var i = 0; i < allBinds.length; i++) {
var bind = allBinds[i];
if (bind.action === currentAction)
continue;
for (var k = 0; k < bind.keys.length; k++) {
if (normalizeKeyCombo(bind.keys[k].key, modKey) === normalizedKey) {
if (normalizeKeyCombo(bind.keys[k].key) === normalizedKey) {
conflicts.push({
action: bind.action,
desc: bind.desc || bind.action
-107
View File
@@ -1,107 +0,0 @@
.pragma library
// English language name -> ISO 639-1 code, for abbreviating xkb layout names.
const LANG_CODES = {
"afrikaans": "af",
"albanian": "sq",
"amharic": "am",
"arabic": "ar",
"armenian": "hy",
"azerbaijani": "az",
"basque": "eu",
"belarusian": "be",
"bengali": "bn",
"bosnian": "bs",
"bulgarian": "bg",
"burmese": "my",
"catalan": "ca",
"chinese": "zh",
"croatian": "hr",
"czech": "cs",
"danish": "da",
"dutch": "nl",
"english": "en",
"esperanto": "eo",
"estonian": "et",
"filipino": "fil",
"finnish": "fi",
"french": "fr",
"galician": "gl",
"georgian": "ka",
"german": "de",
"greek": "el",
"gujarati": "gu",
"hausa": "ha",
"hebrew": "he",
"hindi": "hi",
"hungarian": "hu",
"icelandic": "is",
"igbo": "ig",
"indonesian": "id",
"irish": "ga",
"italian": "it",
"japanese": "ja",
"javanese": "jv",
"kannada": "kn",
"kazakh": "kk",
"khmer": "km",
"korean": "ko",
"kurdish": "ku",
"kyrgyz": "ky",
"lao": "lo",
"latvian": "lv",
"lithuanian": "lt",
"luxembourgish": "lb",
"macedonian": "mk",
"malay": "ms",
"malayalam": "ml",
"maltese": "mt",
"maori": "mi",
"marathi": "mr",
"mongolian": "mn",
"nepali": "ne",
"norwegian": "no",
"pashto": "ps",
"persian": "fa",
"iranian": "fa",
"farsi": "fa",
"polish": "pl",
"portuguese": "pt",
"punjabi": "pa",
"romanian": "ro",
"russian": "ru",
"serbian": "sr",
"sindhi": "sd",
"sinhala": "si",
"slovak": "sk",
"slovenian": "sl",
"somali": "so",
"spanish": "es",
"swahili": "sw",
"swedish": "sv",
"tajik": "tg",
"tamil": "ta",
"tatar": "tt",
"telugu": "te",
"thai": "th",
"tibetan": "bo",
"turkish": "tr",
"turkmen": "tk",
"ukrainian": "uk",
"urdu": "ur",
"uyghur": "ug",
"uzbek": "uz",
"vietnamese": "vi",
"welsh": "cy",
"yiddish": "yi",
"yoruba": "yo",
"zulu": "zu"
};
function layoutCode(layoutName) {
if (!layoutName)
return "";
const lang = layoutName.split(" ")[0].toLowerCase();
const code = LANG_CODES[lang] || lang.substring(0, 2);
return code.toUpperCase();
}
-3
View File
@@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound
import Quickshell
import QtCore
import QtQuick
import qs.Services
Singleton {
@@ -20,8 +19,6 @@ Singleton {
readonly property url imagecache: `${cache}/imagecache`
Component.onCompleted: mkdir(imagecache)
function stringify(path: url): string {
return path.toString().replace(/%20/g, " ");
}
+7 -29
View File
@@ -79,6 +79,12 @@ Singleton {
function _closePopout(popout) {
try {
if (popout?.hoverDismissEnabled) {
if (typeof popout.closeFromHoverDismiss === "function") {
popout.closeFromHoverDismiss();
return;
}
}
if (popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = false;
switch (true) {
@@ -98,20 +104,6 @@ Singleton {
}
}
function _dismissPopoutFromHover(popout) {
try {
if (!popout || popout.hoverDismissEnabled !== true)
return;
if (typeof popout.closeFromHoverDismiss === "function") {
popout.closeFromHoverDismiss();
return;
}
_closePopout(popout);
} catch (e) {
return;
}
}
function _isStale(popout) {
try {
if (!popout || !("shouldBeVisible" in popout))
@@ -182,19 +174,6 @@ Singleton {
_closePopout(popout);
}
function dismissHoverPopoutForScreen(screen) {
if (!screen)
return;
const screenName = screen.name;
const popout = currentPopoutsByScreen[screenName];
if (!popout || _isStale(popout)) {
currentPopoutsByScreen[screenName] = null;
currentPopoutTriggers[screenName] = null;
return;
}
_dismissPopoutFromHover(popout);
}
function cancelHoverDismiss(screen) {
const popout = getActivePopout(screen);
if (popout?.cancelHoverDismiss)
@@ -212,8 +191,7 @@ Singleton {
const p = getActivePopout(screen);
if (!p || !_isPopoutPresented(p))
return false;
const dismissSuspended = p.effectiveHoverDismissSuspended ?? p.hoverDismissSuspended;
return p.hoverDismissEnabled === false || dismissSuspended === true;
return p.hoverDismissEnabled === false || p.hoverDismissSuspended === true;
}
function isCurrentPopout(popout, screenName) {
+2 -17
View File
@@ -120,18 +120,12 @@ Singleton {
function setMonitorScrollPosition(screenName, scrollX, scrollY) {
var newPositions = Object.assign({}, monitorScrollPositions);
newPositions[screenName] = {
scrollX: scrollX,
scrollY: scrollY
};
newPositions[screenName] = { scrollX: scrollX, scrollY: scrollY };
monitorScrollPositions = newPositions;
}
function getMonitorScrollPosition(screenName) {
return monitorScrollPositions[screenName] || {
scrollX: 50,
scrollY: 50
};
return monitorScrollPositions[screenName] || { scrollX: 50, scrollY: 50 };
}
function clearMonitorScrollPosition(screenName) {
@@ -215,8 +209,6 @@ Singleton {
property string locale: ""
property string timeLocale: ""
property string notepadLastMode: ""
property string launcherLastMode: "all"
property string launcherLastFileSearchType: "all"
property string launcherLastQuery: ""
@@ -1224,13 +1216,6 @@ Singleton {
I18n.useLocale(locale, locale.startsWith("en") ? "" : I18n.folder + "/" + locale + ".json");
}
function setNotepadLastMode(mode) {
if (notepadLastMode === mode)
return;
notepadLastMode = mode;
saveSettings();
}
function setLauncherLastMode(mode) {
launcherLastMode = mode;
saveSettings();
+20 -55
View File
@@ -15,7 +15,7 @@ Singleton {
id: root
readonly property var log: Log.scoped("SettingsData")
readonly property int settingsConfigVersion: 12
readonly property int settingsConfigVersion: 11
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -177,12 +177,10 @@ Singleton {
property int niriLayoutRadiusOverride: -1
property int niriLayoutBorderSize: -1
property int hyprlandLayoutGapsOverride: -1
property int hyprlandLayoutGapsOutOverride: -1
property int hyprlandLayoutRadiusOverride: -1
property int hyprlandLayoutBorderSize: -1
property bool hyprlandResizeOnBorder: false
property int mangoLayoutGapsOverride: -1
property int mangoLayoutGapsOutOverride: -1
property int mangoLayoutRadiusOverride: -1
property int mangoLayoutBorderSize: -1
property bool mangoTrackpadNaturalScrolling: true
@@ -462,7 +460,6 @@ Singleton {
property bool clockCompactMode: false
property int focusedWindowSize: 1
property bool focusedWindowCompactMode: false
property bool focusedWindowShowIcon: true
property bool runningAppsCompactMode: true
property int barMaxVisibleApps: 0
property int barMaxVisibleRunningApps: 0
@@ -497,8 +494,6 @@ Singleton {
property string greeterLockDateFormat: ""
property string greeterFontFamily: ""
property string greeterWallpaperFillMode: ""
property bool greeterSyncPending: false
property var greeterSyncBaseline: ({})
property int mediaSize: 1
property string appLauncherViewMode: "list"
@@ -753,16 +748,12 @@ Singleton {
property bool batteryNotifyCritical: true
property int batteryLowThreshold: 20
property bool batteryNotifyLow: false
property int batteryChargeLimitNotificationType: 0
property int batteryLowNotificationType: 0
property int batteryCriticalNotificationType: 1
property int batteryNotificationType: 0
property bool batteryAutoPowerSaver: false
property bool showBatteryPercent: true
property bool showBatteryPercentOnlyOnBattery: false
property bool showBatteryTime: false
property bool showBatteryTimeOnlyOnBattery: false
property bool batteryPillStyle: false
property bool batteryPillPercentSign: false
property bool lockBeforeSuspend: false
property bool loginctlLockIntegration: true
property bool fadeToLockEnabled: true
@@ -894,14 +885,12 @@ Singleton {
readonly property bool greeterU2fReady: Processes.greeterU2fReady
readonly property string greeterU2fReason: Processes.greeterU2fReason
readonly property string greeterU2fSource: Processes.greeterU2fSource
property string lockScreenActiveMonitor: "all"
property string lockScreenInactiveColor: "#000000"
property int lockScreenNotificationMode: 0
property bool lockScreenVideoEnabled: false
property string lockScreenVideoPath: ""
property bool lockScreenVideoCycling: false
property string lockScreenWallpaperPath: ""
property string lockScreenWallpaperFillMode: ""
property string lockScreenFontFamily: ""
property bool hideBrightnessSlider: false
property int notificationTimeoutLow: 5000
@@ -1027,6 +1016,17 @@ Singleton {
}
]
// Standalone bar xray is unsafe when windows can render beneath its surface
function _standaloneBarXrayAvailable(configs) {
const list = configs || [];
const activeBars = list.filter(c => c && c.enabled && (c.visible ?? true));
const gapsOverride = (typeof CompositorService !== "undefined" && CompositorService.isHyprland) ? hyprlandLayoutGapsOverride : niriLayoutGapsOverride;
const layoutGaps = gapsOverride >= 0 ? gapsOverride : Math.max(4, (list[0]?.spacing ?? 4));
return activeBars.every(c => !c.autoHide && !(c.useOverlayLayer ?? false) && (c.spacing ?? 4) + (c.bottomGap ?? 0) + layoutGaps >= 0);
}
readonly property bool standaloneBarXrayAvailable: _standaloneBarXrayAvailable(barConfigs)
property bool desktopClockEnabled: false
property string desktopClockStyle: "analog"
property real desktopClockTransparency: 0.8
@@ -1677,32 +1677,6 @@ Singleton {
});
}
function markGreeterSyncPending(who, key, oldValue) {
if (isGreeterMode)
return;
if (!(key in greeterSyncBaseline)) {
var baseline = greeterSyncBaseline;
baseline[key] = oldValue;
greeterSyncBaseline = baseline;
}
greeterSyncPending = true;
}
function clearGreeterSyncPending() {
greeterSyncBaseline = {};
greeterSyncPending = false;
saveSettings();
}
function revertGreeterSyncPending() {
for (var key in greeterSyncBaseline) {
root[key] = greeterSyncBaseline[key];
}
greeterSyncBaseline = {};
greeterSyncPending = false;
saveSettings();
}
readonly property var _hooks: ({
"applyStoredTheme": applyStoredTheme,
"regenSystemThemes": regenSystemThemes,
@@ -1711,8 +1685,7 @@ Singleton {
"updateBarConfigs": updateBarConfigs,
"updateCompositorCursor": updateCompositorCursor,
"scheduleAuthApply": scheduleAuthApply,
"scheduleGreeterAutoLoginSync": scheduleGreeterAutoLoginSync,
"markGreeterSyncPending": markGreeterSyncPending
"scheduleGreeterAutoLoginSync": scheduleGreeterAutoLoginSync
})
function set(key, value) {
@@ -1738,18 +1711,6 @@ Singleton {
}
}
if (obj?.lockScreenActiveMonitor !== undefined) {
var oldVal = obj.lockScreenActiveMonitor;
if (oldVal && oldVal !== "all") {
if (!obj.screenPreferences)
obj.screenPreferences = {};
if (obj.screenPreferences.lockScreen === undefined) {
obj.screenPreferences.lockScreen = [oldVal];
}
}
delete obj.lockScreenActiveMonitor;
}
Store.parse(root, obj);
if (obj?.directionalAnimationMode === 3 && frameMode !== "connected")
@@ -2468,13 +2429,17 @@ Singleton {
if (index === -1)
return;
const positionChanged = updates.position !== undefined && configs[index].position !== updates.position;
const barXrayTargetWasAvailable = _standaloneBarXrayAvailable(configs);
if (updates.autoHide === false || updates.visible === false)
setBarIpcReveal(barId, false);
Object.assign(configs[index], updates);
barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
const sanitizedConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
barConfigs = sanitizedConfigs;
updateBarConfigs();
if (!frameEnabled && _standaloneBarXrayAvailable(sanitizedConfigs) !== barXrayTargetWasAvailable)
updateCompositorLayout();
if (positionChanged) {
NotificationService.dismissAllPopups();
}
+7 -14
View File
@@ -142,8 +142,6 @@ Singleton {
Component.onCompleted: {
Quickshell.execDetached(["mkdir", "-p", stateDir]);
if (typeof SessionData === "undefined" || !SessionData.isGreeterMode)
Quickshell.execDetached([shellDir + "/scripts/gtk.sh", configDir, "", shellDir, "assets-only"]);
Proc.runCommand("matugenCheck", ["sh", "-c", "command -v matugen"], (output, code) => {
matugenAvailable = (code === 0) && !envDisableMatugen;
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
@@ -1803,11 +1801,8 @@ Singleton {
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
if (currentTheme === dynamic) {
if (!rawWallpaperPath) {
log.warn("Auto theme has no wallpaper - skipping matugen, syncing portal mode only");
PortalService.setLightMode(isLight);
if (!rawWallpaperPath)
return;
}
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot";
const kind = rawWallpaperPath.startsWith("#") ? "hex" : "image";
setDesiredTheme(kind, rawWallpaperPath, isLight, iconTheme, selectedMatugenType, null);
@@ -1832,21 +1827,19 @@ Singleton {
const storedLight = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.light || lightDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults);
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
const darkAccentId = storedDark.accent || darkDefaults.accent || "";
const lightAccentId = storedLight.accent || lightDefaults.accent || "";
const accentId = storedDark.accent || darkDefaults.accent || "";
const darkFlavor = findVariant(customThemeRawData.variants.flavors, darkFlavorId);
const lightFlavor = findVariant(customThemeRawData.variants.flavors, lightFlavorId);
const darkAccent = findAccent(customThemeRawData.variants.accents, darkAccentId);
const lightAccent = findAccent(customThemeRawData.variants.accents, lightAccentId);
const accent = findAccent(customThemeRawData.variants.accents, accentId);
if (darkFlavor) {
darkTheme = mergeColors(darkTheme, darkFlavor.dark || {});
if (darkAccent)
darkTheme = mergeColors(darkTheme, darkAccent[darkFlavor.id] || {});
if (accent)
darkTheme = mergeColors(darkTheme, accent[darkFlavor.id] || {});
}
if (lightFlavor) {
lightTheme = mergeColors(lightTheme, lightFlavor.light || {});
if (lightAccent)
lightTheme = mergeColors(lightTheme, lightAccent[lightFlavor.id] || {});
if (accent)
lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {});
}
} else if (customThemeRawData.variants.options) {
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
-6
View File
@@ -32,8 +32,6 @@ Singleton {
showBatteryPercentOnlyOnBattery: false,
showBatteryTime: false,
showBatteryTimeOnlyOnBattery: false,
batteryPillStyle: false,
batteryPillPercentSign: false,
showPrinterIcon: false,
showScreenSharingIcon: true
};
@@ -97,10 +95,6 @@ Singleton {
item.showBatteryTime = order[i].showBatteryTime;
if (isObj && order[i].showBatteryTimeOnlyOnBattery !== undefined)
item.showBatteryTimeOnlyOnBattery = order[i].showBatteryTimeOnlyOnBattery;
if (isObj && order[i].batteryPillStyle !== undefined)
item.batteryPillStyle = order[i].batteryPillStyle;
if (isObj && order[i].batteryPillPercentSign !== undefined)
item.batteryPillPercentSign = order[i].batteryPillPercentSign;
if (isObj && order[i].showPrinterIcon !== undefined)
item.showPrinterIcon = order[i].showPrinterIcon;
if (isObj && order[i].showScreenSharingIcon !== undefined)
+27 -8
View File
@@ -332,6 +332,7 @@ Singleton {
property bool greeterAutoLoginSyncRerunRequested: false
property string greeterAutoLoginSyncStdout: ""
property string greeterAutoLoginSyncStderr: ""
property string greeterAutoLoginSyncTerminalFallbackStderr: ""
function scheduleGreeterAutoLoginSync() {
if (!settingsRoot || settingsRoot.isGreeterMode)
@@ -354,16 +355,15 @@ Singleton {
greeterAutoLoginSyncRerunRequested = false;
greeterAutoLoginSyncStdout = "";
greeterAutoLoginSyncStderr = "";
greeterAutoLoginSyncTerminalFallbackStderr = "";
greeterAutoLoginSyncRunning = true;
greeterAutoLoginSyncSudoProbeProcess.running = true;
}
function deferGreeterAutoLoginSyncToPill(details) {
ToastService.dismissCategory("greeter-autologin-sync");
if (settingsRoot)
settingsRoot.set("greeterSyncPending", true);
ToastService.showWarning(I18n.tr("Auto-login change needs a sync"), I18n.tr("Administrator access is required. Use the Sync button in Settings → Greeter to apply.") + (details ? "\n\n" + details : ""), "dms greeter sync --autologin", "greeter-autologin-sync");
finishGreeterAutoLoginSync();
function launchGreeterAutoLoginSyncTerminalFallback(details) {
ToastService.showWarning(I18n.tr("Opening terminal to update greetd"), I18n.tr("DMS needs administrator access. The terminal closes automatically when done.") + (details ? "\n\n" + details : ""), "dms greeter sync --autologin", "greeter-autologin-sync");
greeterAutoLoginSyncTerminalFallbackStderr = "";
greeterAutoLoginSyncTerminalFallbackProcess.running = true;
}
function greeterAutoLoginSyncSuccessToast(details) {
@@ -559,7 +559,7 @@ Singleton {
details = out;
if (err !== "")
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
root.deferGreeterAutoLoginSyncToPill(details);
root.launchGreeterAutoLoginSyncTerminalFallback(details);
}
}
@@ -575,7 +575,26 @@ Singleton {
return;
}
root.deferGreeterAutoLoginSyncToPill("");
root.launchGreeterAutoLoginSyncTerminalFallback();
}
}
property var greeterAutoLoginSyncTerminalFallbackProcess: Process {
command: ["dms", "greeter", "sync", "--terminal", "--yes", "--autologin"]
running: false
stderr: StdioCollector {
onStreamFinished: root.greeterAutoLoginSyncTerminalFallbackStderr = text || ""
}
onExited: exitCode => {
if (exitCode === 0) {
root.greeterAutoLoginSyncSuccessToast("");
} else {
let details = (root.greeterAutoLoginSyncTerminalFallbackStderr || "").trim();
ToastService.showError(I18n.tr("Couldn't open a terminal for the auto-login update.") + " (exit " + exitCode + ")", details, "dms greeter sync --autologin", "greeter-autologin-sync");
}
root.finishGreeterAutoLoginSync();
}
}
@@ -88,8 +88,6 @@ var SPEC = {
locale: { def: "", onChange: "updateLocale" },
timeLocale: { def: "" },
notepadLastMode: { def: "" },
launcherLastMode: { def: "all" },
launcherLastFileSearchType: { def: "all" },
launcherLastQuery: { def: "" },

Some files were not shown because too many files have changed in this diff Show More