mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 03:28:28 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a95206bf41 |
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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{}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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!" && \
|
||||
|
||||
@@ -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!" && \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
${
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ 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 |
|
||||
|
||||
@@ -43,7 +43,6 @@ post_install() {
|
||||
# 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)
|
||||
|
||||
@@ -33,7 +33,6 @@ post_install() {
|
||||
# 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -133,16 +133,11 @@
|
||||
$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
|
||||
|
||||
# Snapshot pre-wrap Qt paths for launched apps to restore.
|
||||
wrapProgram $out/bin/dms \
|
||||
--add-flags "-c $out/share/quickshell/dms" \
|
||||
--run 'export DMS_ORIG_NIXPKGS_QT6_QML_IMPORT_PATH="''${NIXPKGS_QT6_QML_IMPORT_PATH:-}"' \
|
||||
--run 'export DMS_ORIG_QT_PLUGIN_PATH="''${QT_PLUGIN_PATH:-}"' \
|
||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
|
||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: false
|
||||
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
|
||||
@@ -1688,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,
|
||||
@@ -1722,8 +1685,7 @@ Singleton {
|
||||
"updateBarConfigs": updateBarConfigs,
|
||||
"updateCompositorCursor": updateCompositorCursor,
|
||||
"scheduleAuthApply": scheduleAuthApply,
|
||||
"scheduleGreeterAutoLoginSync": scheduleGreeterAutoLoginSync,
|
||||
"markGreeterSyncPending": markGreeterSyncPending
|
||||
"scheduleGreeterAutoLoginSync": scheduleGreeterAutoLoginSync
|
||||
})
|
||||
|
||||
function set(key, value) {
|
||||
@@ -1749,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")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,12 +29,10 @@ var SPEC = {
|
||||
niriLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||
niriLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||
hyprlandLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||
hyprlandLayoutGapsOutOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||
hyprlandLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||
hyprlandLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||
hyprlandResizeOnBorder: { def: false, onChange: "updateCompositorLayout" },
|
||||
mangoLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||
mangoLayoutGapsOutOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||
mangoTrackpadNaturalScrolling: { def: true, onChange: "updateCompositorCursor" },
|
||||
@@ -102,8 +100,6 @@ var SPEC = {
|
||||
showBatteryPercentOnlyOnBattery: { def: false },
|
||||
showBatteryTime: { def: false },
|
||||
showBatteryTimeOnlyOnBattery: { def: false },
|
||||
batteryPillStyle: { def: false },
|
||||
batteryPillPercentSign: { def: false },
|
||||
showControlCenterButton: { def: true },
|
||||
showCapsLockIndicator: { def: true },
|
||||
|
||||
@@ -192,7 +188,6 @@ var SPEC = {
|
||||
clockCompactMode: { def: false },
|
||||
focusedWindowCompactMode: { def: false },
|
||||
focusedWindowSize: { def: 1 },
|
||||
focusedWindowShowIcon: { def: false },
|
||||
runningAppsCompactMode: { def: true },
|
||||
barMaxVisibleApps: { def: 0 },
|
||||
barMaxVisibleRunningApps: { def: 0 },
|
||||
@@ -223,20 +218,18 @@ var SPEC = {
|
||||
centeringMode: { def: "index" },
|
||||
clockDateFormat: { def: "" },
|
||||
lockDateFormat: { def: "" },
|
||||
greeterRememberLastSession: { def: true, onChange: "markGreeterSyncPending" },
|
||||
greeterRememberLastUser: { def: true, onChange: "markGreeterSyncPending" },
|
||||
greeterRememberLastSession: { def: true },
|
||||
greeterRememberLastUser: { def: true },
|
||||
greeterAutoLogin: { def: false, onChange: "scheduleGreeterAutoLoginSync" },
|
||||
greeterEnableFprint: { def: false, onChange: "scheduleAuthApply" },
|
||||
greeterEnableU2f: { def: false, onChange: "scheduleAuthApply" },
|
||||
greeterWallpaperPath: { def: "", onChange: "markGreeterSyncPending" },
|
||||
greeterUse24HourClock: { def: true, onChange: "markGreeterSyncPending" },
|
||||
greeterShowSeconds: { def: false, onChange: "markGreeterSyncPending" },
|
||||
greeterPadHours12Hour: { def: false, onChange: "markGreeterSyncPending" },
|
||||
greeterLockDateFormat: { def: "", onChange: "markGreeterSyncPending" },
|
||||
greeterFontFamily: { def: "", onChange: "markGreeterSyncPending" },
|
||||
greeterWallpaperFillMode: { def: "", onChange: "markGreeterSyncPending" },
|
||||
greeterSyncPending: { def: false },
|
||||
greeterSyncBaseline: { def: {} },
|
||||
greeterWallpaperPath: { def: "" },
|
||||
greeterUse24HourClock: { def: true },
|
||||
greeterShowSeconds: { def: false },
|
||||
greeterPadHours12Hour: { def: false },
|
||||
greeterLockDateFormat: { def: "" },
|
||||
greeterFontFamily: { def: "" },
|
||||
greeterWallpaperFillMode: { def: "" },
|
||||
mediaSize: { def: 1 },
|
||||
|
||||
appLauncherViewMode: { def: "list" },
|
||||
@@ -342,9 +335,7 @@ var SPEC = {
|
||||
batteryNotifyCritical: { def: true },
|
||||
batteryLowThreshold: { def: 20 },
|
||||
batteryNotifyLow: { def: false },
|
||||
batteryChargeLimitNotificationType: { def: 0 },
|
||||
batteryLowNotificationType: { def: 0 },
|
||||
batteryCriticalNotificationType: { def: 1 },
|
||||
batteryNotificationType: { def: 0 },
|
||||
batteryAutoPowerSaver: { def: false },
|
||||
lockBeforeSuspend: { def: false },
|
||||
loginctlLockIntegration: { def: true },
|
||||
@@ -456,14 +447,12 @@ var SPEC = {
|
||||
maxFprintTries: { def: 15 },
|
||||
enableU2f: { def: false, onChange: "scheduleAuthApply" },
|
||||
u2fMode: { def: "or" },
|
||||
lockScreenActiveMonitor: { def: "all" },
|
||||
lockScreenInactiveColor: { def: "#000000" },
|
||||
lockScreenNotificationMode: { def: 0 },
|
||||
lockScreenVideoEnabled: { def: false },
|
||||
lockScreenVideoPath: { def: "" },
|
||||
lockScreenVideoCycling: { def: false },
|
||||
lockScreenWallpaperPath: { def: "" },
|
||||
lockScreenWallpaperFillMode: { def: "" },
|
||||
lockScreenFontFamily: { def: "" },
|
||||
hideBrightnessSlider: { def: false },
|
||||
|
||||
notificationTimeoutLow: { def: 5000 },
|
||||
@@ -670,11 +659,10 @@ function getValidKeys() {
|
||||
function set(root, key, value, saveFn, hooks) {
|
||||
if (!(key in SPEC)) return;
|
||||
if (value === undefined || value === null) value = SPEC[key].def;
|
||||
var oldValue = root[key];
|
||||
root[key] = value;
|
||||
var hookName = SPEC[key].onChange;
|
||||
if (hookName && hooks && hooks[hookName]) {
|
||||
hooks[hookName](root, key, oldValue);
|
||||
hooks[hookName](root);
|
||||
}
|
||||
saveFn();
|
||||
}
|
||||
|
||||
@@ -252,17 +252,6 @@ function migrateToVersion(obj, targetVersion) {
|
||||
settings.configVersion = 11;
|
||||
}
|
||||
|
||||
if (currentVersion < 12) {
|
||||
console.info("Migrating settings from version", currentVersion, "to version 12");
|
||||
if (settings.batteryNotificationType !== undefined) {
|
||||
settings.batteryChargeLimitNotificationType = settings.batteryNotificationType;
|
||||
settings.batteryLowNotificationType = settings.batteryNotificationType;
|
||||
settings.batteryCriticalNotificationType = settings.batteryNotificationType;
|
||||
delete settings.batteryNotificationType;
|
||||
}
|
||||
settings.configVersion = 12;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
|
||||
@@ -385,22 +385,6 @@ Item {
|
||||
return "NOTEPAD_OPEN_FAILED";
|
||||
}
|
||||
|
||||
function openFile(path: string): string {
|
||||
if (!path)
|
||||
return open();
|
||||
if (SettingsData.notepadDefaultMode === "popout") {
|
||||
PopoutService.openNotepadPopoutWithFile(path);
|
||||
return "NOTEPAD_OPEN_FILE_SUCCESS";
|
||||
}
|
||||
var instance = getActiveNotepadInstance();
|
||||
if (instance) {
|
||||
instance.show();
|
||||
instance.loadedItem?.openExternalFile(path);
|
||||
return "NOTEPAD_OPEN_FILE_SUCCESS";
|
||||
}
|
||||
return "NOTEPAD_OPEN_FILE_FAILED";
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (SettingsData.notepadDefaultMode === "popout") {
|
||||
PopoutService.notepadPopout?.hide();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Widgets
|
||||
@@ -24,7 +23,6 @@ DankModal {
|
||||
property var rememberMimeTypes: []
|
||||
property bool rememberChoice: false
|
||||
property var mimeMatchedAppIds: []
|
||||
property var mimeMatchedRawIds: []
|
||||
|
||||
signal applicationSelected(var app, string targetData)
|
||||
|
||||
@@ -63,7 +61,6 @@ DankModal {
|
||||
|
||||
function fetchMimeMatches() {
|
||||
mimeMatchedAppIds = [];
|
||||
mimeMatchedRawIds = [];
|
||||
const queriedMime = mimeType;
|
||||
if (queriedMime.length === 0)
|
||||
return;
|
||||
@@ -77,7 +74,6 @@ DankModal {
|
||||
return;
|
||||
}
|
||||
const ids = (response.result && response.result.desktopIds) || [];
|
||||
mimeMatchedRawIds = ids;
|
||||
mimeMatchedAppIds = ids.map(_normAppId);
|
||||
updateApplicationList();
|
||||
});
|
||||
@@ -97,13 +93,11 @@ DankModal {
|
||||
const hasMimeMatches = mimeMatchedAppIds.length > 0;
|
||||
const lowerQuery = searchQuery.toLowerCase();
|
||||
let filteredApps = [];
|
||||
const listedIds = new Set();
|
||||
|
||||
for (const app of apps) {
|
||||
if (!app)
|
||||
continue;
|
||||
const appId = _normAppId(app.id || app.execString || app.exec || "");
|
||||
listedIds.add(appId);
|
||||
const mimeIdMatch = hasMimeMatches && mimeMatchedAppIds.includes(appId);
|
||||
const mimeFieldMatch = hasMime && _appMatchesMime(app, mimeType);
|
||||
const mimeMatch = mimeIdMatch || mimeFieldMatch;
|
||||
@@ -141,28 +135,6 @@ DankModal {
|
||||
});
|
||||
}
|
||||
|
||||
// NoDisplay entries are excluded from DesktopEntries.applications but
|
||||
// remain valid mime handlers; resolve them by id so they stay pickable
|
||||
for (const rawId of mimeMatchedRawIds) {
|
||||
const normId = _normAppId(rawId);
|
||||
if (normId === "dms-open" || listedIds.has(normId))
|
||||
continue;
|
||||
const entry = DesktopEntries.byId(rawId) || DesktopEntries.heuristicLookup(rawId);
|
||||
if (!entry)
|
||||
continue;
|
||||
const name = entry.name || "";
|
||||
if (searchQuery !== "" && !name.toLowerCase().includes(lowerQuery))
|
||||
continue;
|
||||
filteredApps.push({
|
||||
name: name,
|
||||
icon: entry.icon || "application-x-executable",
|
||||
exec: entry.execString || "",
|
||||
startupClass: entry.startupClass || "",
|
||||
appData: entry,
|
||||
mimeMatch: true
|
||||
});
|
||||
}
|
||||
|
||||
filteredApps.sort((a, b) => {
|
||||
if (a.mimeMatch !== b.mimeMatch) {
|
||||
return a.mimeMatch ? -1 : 1;
|
||||
|
||||
@@ -8,7 +8,6 @@ Item {
|
||||
id: clipboardContent
|
||||
|
||||
required property var modal
|
||||
property var transientSurfaceTracker: null
|
||||
|
||||
property alias searchField: searchField
|
||||
property alias clipboardListView: clipboardListView
|
||||
@@ -67,7 +66,7 @@ Item {
|
||||
contextMenu.hide();
|
||||
}
|
||||
|
||||
readonly property bool contextMenuActive: contextMenu.renderActive
|
||||
readonly property bool contextMenuActive: contextMenu.openState
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -75,7 +74,6 @@ Item {
|
||||
id: contextMenu
|
||||
modal: clipboardContent.modal
|
||||
parentHandler: clipboardContent
|
||||
transientSurfaceTracker: clipboardContent.transientSurfaceTracker
|
||||
}
|
||||
|
||||
Column {
|
||||
@@ -396,7 +394,7 @@ Item {
|
||||
}
|
||||
|
||||
sourceComponent: ClipboardKeyboardHints {
|
||||
pasteAvailable: modal.pasteAvailable
|
||||
wtypeAvailable: modal.wtypeAvailable
|
||||
enterToPaste: SettingsData.clipboardEnterToPaste
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ Item {
|
||||
property real anchorY: 0
|
||||
property bool openState: false
|
||||
property bool renderActive: false
|
||||
property var transientSurfaceTracker: null
|
||||
readonly property alias contextWindow: menuWindow
|
||||
readonly property bool blurActive: renderActive && openState && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
|
||||
|
||||
readonly property bool hasPinnedDuplicate: !!entry && !entry.pinned && ClipboardService.getPinnedEntryByHash(entry.hash) !== null
|
||||
@@ -92,18 +90,6 @@ Item {
|
||||
readonly property real effectiveMenuHeight: Math.min(maxMenuHeight, naturalMenuHeight)
|
||||
readonly property bool menuScrolls: naturalMenuHeight > effectiveMenuHeight + 0.5
|
||||
|
||||
onRenderActiveChanged: transientSurfaceTracker?.setActive(root, renderActive, contextWindow)
|
||||
Component.onDestruction: transientSurfaceTracker?.unregister(root)
|
||||
|
||||
Connections {
|
||||
target: root.transientSurfaceTracker
|
||||
ignoreUnknownSignals: true
|
||||
|
||||
function onCloseRequested() {
|
||||
root.hide();
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: menuTextMetrics
|
||||
text: root.longestMenuText
|
||||
@@ -272,8 +258,8 @@ Item {
|
||||
height: root.effectiveMenuHeight
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
|
||||
border.width: BlurService.enabled ? BlurService.borderWidth : 1
|
||||
opacity: root.openState ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
|
||||
@@ -496,7 +496,7 @@ Item {
|
||||
implicitHeight: saveMenuPasteRow.implicitHeight + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: saveMenuPasteArea.containsMouse ? Theme.surfaceVariant : Theme.withAlpha(Theme.surfaceVariant, 0)
|
||||
opacity: modal.pasteAvailable ? 1 : 0.5
|
||||
opacity: modal.wtypeAvailable ? 1 : 0.5
|
||||
|
||||
Row {
|
||||
id: saveMenuPasteRow
|
||||
@@ -522,7 +522,7 @@ Item {
|
||||
id: saveMenuPasteArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: modal.pasteAvailable
|
||||
enabled: modal.wtypeAvailable
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: {
|
||||
saveMenu.close();
|
||||
|
||||
@@ -49,15 +49,6 @@ Rectangle {
|
||||
cornerRadius: root.radius
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
const scenePos = mapToItem(null, mouse.x, mouse.y);
|
||||
contextMenuRequested(scenePos.x, scenePos.y);
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: indexBadge
|
||||
anchors.left: parent.left
|
||||
@@ -243,4 +234,13 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
const scenePos = mapToItem(null, mouse.x, mouse.y);
|
||||
contextMenuRequested(scenePos.x, scenePos.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ FocusScope {
|
||||
|
||||
property var clearConfirmDialog: null
|
||||
property var surfaceHost: null
|
||||
property var transientSurfaceTracker: null
|
||||
|
||||
property string activeTab: "recents"
|
||||
property bool showKeyboardHints: false
|
||||
@@ -21,7 +20,7 @@ FocusScope {
|
||||
property string activeFilter: SettingsData.clipboardRememberTypeFilter ? SettingsData.clipboardTypeFilter : "all"
|
||||
|
||||
readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
|
||||
readonly property bool pasteAvailable: ClipboardService.pasteAvailable
|
||||
readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable
|
||||
readonly property int totalCount: ClipboardService.totalCount
|
||||
readonly property var clipboardEntries: ClipboardService.clipboardEntries
|
||||
readonly property var pinnedEntries: ClipboardService.pinnedEntries
|
||||
@@ -199,7 +198,6 @@ FocusScope {
|
||||
id: historyContent
|
||||
anchors.fill: parent
|
||||
modal: root
|
||||
transientSurfaceTracker: root.transientSurfaceTracker
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,6 @@ DankModal {
|
||||
content: Component {
|
||||
ClipboardHistoryContent {
|
||||
surfaceHost: clipboardHistoryModal
|
||||
transientSurfaceTracker: clipboardHistoryModal.transientSurfaceTracker
|
||||
clearConfirmDialog: clearConfirmDialog
|
||||
onCloseRequested: clipboardHistoryModal.hide()
|
||||
onInstantCloseRequested: clipboardHistoryModal.instantHide()
|
||||
|
||||
@@ -141,7 +141,6 @@ DankPopout {
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
surfaceHost: root
|
||||
transientSurfaceTracker: root.transientSurfaceTracker
|
||||
clearConfirmDialog: clearConfirmDialog
|
||||
onCloseRequested: root.hide()
|
||||
onInstantCloseRequested: root.hide()
|
||||
|
||||
@@ -5,10 +5,10 @@ import qs.Widgets
|
||||
Rectangle {
|
||||
id: keyboardHints
|
||||
|
||||
property bool pasteAvailable: false
|
||||
property bool wtypeAvailable: false
|
||||
property bool enterToPaste: false
|
||||
readonly property string hintsText: {
|
||||
if (!pasteAvailable)
|
||||
if (!wtypeAvailable)
|
||||
return I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Del: Clear All • Esc: Close");
|
||||
return enterToPaste ? I18n.tr("Ctrl+Tab: Switch Tabs • Ctrl+S: Pin/Unpin • Shift+Enter: Copy • Shift+Del: Clear All • F10: Help • Esc: Close", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("Ctrl+Tab: Switch Tabs • Ctrl+S: Pin/Unpin • Shift+Enter: Paste • Shift+Del: Clear All • F10: Help • Esc: Close");
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import QtQuick
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -39,7 +38,6 @@ Item {
|
||||
property bool keepContentLoaded: false
|
||||
property bool keepPopoutsOpen: false
|
||||
property var customKeyboardFocus: null
|
||||
readonly property alias transientSurfaceTracker: _transientSurfaceTracker
|
||||
property bool useOverlayLayer: false
|
||||
|
||||
signal opened
|
||||
@@ -49,10 +47,6 @@ Item {
|
||||
readonly property var contentLoader: impl.item ? impl.item.contentLoader : null
|
||||
readonly property alias modalFocusScope: _modalFocusScope
|
||||
|
||||
TransientSurfaceTracker {
|
||||
id: _transientSurfaceTracker
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: _modalFocusScope
|
||||
objectName: "modalFocusScope"
|
||||
@@ -62,7 +56,7 @@ Item {
|
||||
|
||||
// Hyprland OnDemand grab delivers keyboard focus to the modal content surface.
|
||||
HyprlandFocusGrab {
|
||||
windows: (root.contentWindow ? [root.contentWindow] : []).concat(root.transientSurfaceTracker?.focusWindows ?? [])
|
||||
windows: root.contentWindow ? [root.contentWindow] : []
|
||||
active: KeyboardFocus.wantsGrab(root.shouldHaveFocus, root.customKeyboardFocus)
|
||||
|
||||
property var restoreToplevel: null
|
||||
@@ -85,13 +79,11 @@ Item {
|
||||
}
|
||||
|
||||
function close() {
|
||||
transientSurfaceTracker?.closeAll?.();
|
||||
if (impl.item)
|
||||
impl.item.close();
|
||||
}
|
||||
|
||||
function instantClose() {
|
||||
transientSurfaceTracker?.closeAll?.();
|
||||
if (impl.item && typeof impl.item.instantClose === "function")
|
||||
impl.item.instantClose();
|
||||
}
|
||||
@@ -183,8 +175,6 @@ Item {
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (!root.shouldBeVisible)
|
||||
root.transientSurfaceTracker?.closeAll?.();
|
||||
if (impl.item && impl.item.shouldBeVisible !== root.shouldBeVisible)
|
||||
impl.item.shouldBeVisible = root.shouldBeVisible;
|
||||
}
|
||||
|
||||
@@ -162,16 +162,37 @@ Item {
|
||||
_publishModalChromeState();
|
||||
}
|
||||
|
||||
property bool _animSyncQueued: false
|
||||
property bool _bodySyncQueued: false
|
||||
|
||||
function _queueFullSync() {
|
||||
_fullSyncPending = true;
|
||||
if (!_syncTimer.running)
|
||||
_syncTimer.restart();
|
||||
}
|
||||
function _queueAnimSync() {
|
||||
_animSyncQueued = true;
|
||||
if (!_syncTimer.running)
|
||||
_syncTimer.restart();
|
||||
}
|
||||
function _queueBodySync() {
|
||||
_bodySyncQueued = true;
|
||||
if (!_syncTimer.running)
|
||||
_syncTimer.restart();
|
||||
}
|
||||
function _flushSync() {
|
||||
if (!_fullSyncPending)
|
||||
return;
|
||||
const fullDirty = _fullSyncPending;
|
||||
const animDirty = _animSyncQueued;
|
||||
const bodyDirty = _bodySyncQueued;
|
||||
_fullSyncPending = false;
|
||||
_syncModalChromeState();
|
||||
_animSyncQueued = false;
|
||||
_bodySyncQueued = false;
|
||||
if (fullDirty)
|
||||
_syncModalChromeState();
|
||||
if (animDirty)
|
||||
_syncModalAnim();
|
||||
if (bodyDirty)
|
||||
_syncModalBody();
|
||||
}
|
||||
|
||||
function _syncModalAnim() {
|
||||
@@ -195,11 +216,10 @@ Item {
|
||||
onFrameOwnsConnectedChromeChanged: _syncModalChromeState()
|
||||
onResolvedConnectedBarSideChanged: _queueFullSync()
|
||||
onShouldBeVisibleChanged: _queueFullSync()
|
||||
// Low resource scalar writes: publish synchronously to stay in the same frame
|
||||
onAlignedXChanged: _syncModalBody()
|
||||
onAlignedYChanged: _syncModalBody()
|
||||
onAlignedWidthChanged: _syncModalBody()
|
||||
onAlignedHeightChanged: _syncModalBody()
|
||||
onAlignedXChanged: _queueBodySync()
|
||||
onAlignedYChanged: _queueBodySync()
|
||||
onAlignedWidthChanged: _queueBodySync()
|
||||
onAlignedHeightChanged: _queueBodySync()
|
||||
|
||||
Connections {
|
||||
target: contentWindow
|
||||
@@ -593,9 +613,9 @@ Item {
|
||||
readonly property real scaleValue: computedScaleCollapsed + (1.0 - computedScaleCollapsed) * morph.openProgress
|
||||
|
||||
onAnimXChanged: if (root.frameOwnsConnectedChrome)
|
||||
root._syncModalAnim()
|
||||
root._queueAnimSync()
|
||||
onAnimYChanged: if (root.frameOwnsConnectedChrome)
|
||||
root._syncModalAnim()
|
||||
root._queueAnimSync()
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
|
||||
@@ -244,11 +244,11 @@ Item {
|
||||
WindowBlur {
|
||||
targetWindow: contentWindow
|
||||
readonly property real s: Math.min(1, modalContainer.scaleValue)
|
||||
// Blur tracks the surface's scaled rect, matching the connected backend.
|
||||
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
|
||||
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
|
||||
blurWidth: root.shouldBeVisible ? modalContainer.width * s : 0
|
||||
blurHeight: root.shouldBeVisible ? modalContainer.height * s : 0
|
||||
readonly property real op: Math.max(0, Math.min(1, (morph.openProgress - 0.06) * 2))
|
||||
blurX: modalContainer.x + modalContainer.width * (1 - s * op) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
|
||||
blurY: modalContainer.y + modalContainer.height * (1 - s * op) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
|
||||
blurWidth: root.shouldBeVisible ? modalContainer.width * s * op : 0
|
||||
blurHeight: root.shouldBeVisible ? modalContainer.height * s * op : 0
|
||||
blurRadius: root.cornerRadius
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,12 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: wtypeProcess
|
||||
command: ["wtype", "-M", "ctrl", "-P", "v", "-p", "v", "-m", "ctrl"]
|
||||
running: false
|
||||
}
|
||||
|
||||
Process {
|
||||
id: copyProcess
|
||||
running: false
|
||||
@@ -155,7 +161,7 @@ Item {
|
||||
id: pasteTimer
|
||||
interval: 200
|
||||
repeat: false
|
||||
onTriggered: ClipboardService.sendPasteKeystroke()
|
||||
onTriggered: wtypeProcess.running = true
|
||||
}
|
||||
|
||||
function pasteSelected() {
|
||||
@@ -173,6 +179,10 @@ Item {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!SessionService.wtypeAvailable) {
|
||||
ToastService.showError(I18n.tr("wtype not available - install wtype for paste support"));
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginId = selectedItem.pluginId;
|
||||
if (!pluginId)
|
||||
|
||||
@@ -35,10 +35,6 @@ Item {
|
||||
property real _frozenMotionX: 0
|
||||
property real _frozenMotionY: 0
|
||||
|
||||
TransientSurfaceTracker {
|
||||
id: transientSurfaces
|
||||
}
|
||||
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
readonly property var effectiveScreen: contentWindow.screen
|
||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||
@@ -503,18 +499,10 @@ Item {
|
||||
}
|
||||
|
||||
// Handles hover dismissal grace periods for edge-hover sessions w/cursor
|
||||
readonly property bool _edgeRetractEnabled: (modalHandle && modalHandle.edgeHoverManaged === true) && spotlightOpen && !isClosing && !transientSurfaces.active
|
||||
readonly property bool _edgeRetractEnabled: (modalHandle && modalHandle.edgeHoverManaged === true) && spotlightOpen && !isClosing
|
||||
property bool _edgeBodyHover: false
|
||||
property bool _edgeArmed: false
|
||||
|
||||
on_EdgeRetractEnabledChanged: {
|
||||
if (!_edgeRetractEnabled) {
|
||||
_edgeRetractGrace.stop();
|
||||
} else if (_edgeArmed && !_edgeBodyHover) {
|
||||
_edgeRetractGrace.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _edgeRetractGrace
|
||||
interval: 150
|
||||
@@ -546,7 +534,9 @@ Item {
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
windows: [contentWindow].concat(transientSurfaces.focusWindows)
|
||||
readonly property var contextMenuWindow: root.spotlightContent?.activeContextMenu?.contextWindow ?? null
|
||||
readonly property bool contextMenuActive: root.spotlightContent?.activeContextMenu?.renderActive ?? false
|
||||
windows: contextMenuActive && contextMenuWindow ? [contentWindow, contextMenuWindow] : [contentWindow]
|
||||
active: root.useHyprlandFocusGrab && root.spotlightOpen
|
||||
|
||||
onCleared: {
|
||||
@@ -879,7 +869,6 @@ Item {
|
||||
sourceComponent: LauncherContent {
|
||||
focus: true
|
||||
parentModal: root
|
||||
transientSurfaceTracker: transientSurfaces
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
|
||||
@@ -26,10 +26,6 @@ Item {
|
||||
property string _pendingMode: ""
|
||||
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
|
||||
TransientSurfaceTracker {
|
||||
id: transientSurfaces
|
||||
}
|
||||
readonly property var effectiveScreen: launcherWindow.screen
|
||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||
@@ -232,7 +228,9 @@ Item {
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
windows: [launcherWindow].concat(transientSurfaces.focusWindows)
|
||||
readonly property var contextMenuWindow: root.spotlightContent?.activeContextMenu?.contextWindow ?? null
|
||||
readonly property bool contextMenuActive: root.spotlightContent?.activeContextMenu?.renderActive ?? false
|
||||
windows: contextMenuActive && contextMenuWindow ? [launcherWindow, contextMenuWindow] : [launcherWindow]
|
||||
active: root.useHyprlandFocusGrab && root.keyboardActive
|
||||
onCleared: {
|
||||
if (spotlightOpen)
|
||||
@@ -484,7 +482,6 @@ Item {
|
||||
sourceComponent: SpotlightLauncherContent {
|
||||
focus: true
|
||||
parentModal: root
|
||||
transientSurfaceTracker: transientSurfaces
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
|
||||
@@ -27,10 +27,6 @@ Item {
|
||||
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
|
||||
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
|
||||
TransientSurfaceTracker {
|
||||
id: transientSurfaces
|
||||
}
|
||||
readonly property var effectiveScreen: launcherWindow.screen
|
||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||
@@ -264,7 +260,9 @@ Item {
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
windows: [launcherWindow].concat(transientSurfaces.focusWindows)
|
||||
readonly property var contextMenuWindow: root.spotlightContent?.activeContextMenu?.contextWindow ?? null
|
||||
readonly property bool contextMenuActive: root.spotlightContent?.activeContextMenu?.renderActive ?? false
|
||||
windows: contextMenuActive && contextMenuWindow ? [launcherWindow, contextMenuWindow] : [launcherWindow]
|
||||
active: root.useHyprlandFocusGrab && root.keyboardActive
|
||||
|
||||
onCleared: {
|
||||
@@ -366,11 +364,11 @@ Item {
|
||||
WindowBlur {
|
||||
targetWindow: launcherWindow
|
||||
readonly property real s: Math.min(1, modalContainer.publishedScale)
|
||||
// Blur tracks the surface's scaled rect
|
||||
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5
|
||||
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5
|
||||
blurWidth: contentVisible ? modalContainer.width * s : 0
|
||||
blurHeight: contentVisible ? modalContainer.height * s : 0
|
||||
readonly property real op: Math.max(0, Math.min(1, (modalContainer.publishedOpacity - 0.06) * 2))
|
||||
blurX: modalContainer.x + modalContainer.width * (1 - s * op) * 0.5
|
||||
blurY: modalContainer.y + modalContainer.height * (1 - s * op) * 0.5
|
||||
blurWidth: contentVisible ? modalContainer.width * s * op : 0
|
||||
blurHeight: contentVisible ? modalContainer.height * s * op : 0
|
||||
blurRadius: root.cornerRadius
|
||||
}
|
||||
|
||||
@@ -532,7 +530,6 @@ Item {
|
||||
sourceComponent: LauncherContent {
|
||||
focus: true
|
||||
parentModal: root
|
||||
transientSurfaceTracker: transientSurfaces
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
|
||||
@@ -18,7 +18,6 @@ FocusScope {
|
||||
property alias resultsList: resultsList
|
||||
property alias actionPanel: actionPanel
|
||||
readonly property alias activeContextMenu: contextMenu
|
||||
property var transientSurfaceTracker: null
|
||||
|
||||
property bool editMode: false
|
||||
property var editingApp: null
|
||||
@@ -44,7 +43,7 @@ FocusScope {
|
||||
}
|
||||
|
||||
function closeTransientUi() {
|
||||
transientSurfaceTracker?.closeAll?.();
|
||||
contextMenu.hide();
|
||||
actionPanel.hide();
|
||||
root.enabled = true;
|
||||
}
|
||||
@@ -124,7 +123,6 @@ FocusScope {
|
||||
controller: root.controller
|
||||
searchField: root.searchField
|
||||
parentHandler: root
|
||||
transientSurfaceTracker: root.transientSurfaceTracker
|
||||
|
||||
onEditAppRequested: app => {
|
||||
root.openEditMode(app);
|
||||
@@ -781,7 +779,6 @@ FocusScope {
|
||||
anchors.fill: parent
|
||||
controller: root.controller
|
||||
leadingSectionHeaderAtBottom: contentHolder.inverted
|
||||
transientSurfaceTracker: root.transientSurfaceTracker
|
||||
|
||||
onItemRightClicked: (index, item, sceneX, sceneY) => {
|
||||
if (item && contextMenu.hasContextMenuActions(item)) {
|
||||
|
||||
@@ -25,7 +25,6 @@ Item {
|
||||
property real anchorY: 0
|
||||
property bool openState: false
|
||||
property bool renderActive: false
|
||||
property var transientSurfaceTracker: null
|
||||
readonly property alias contextWindow: menuWindow
|
||||
readonly property bool blurActive: renderActive && openState && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
|
||||
|
||||
@@ -50,18 +49,6 @@ Item {
|
||||
signal hideRequested
|
||||
signal editAppRequested(var app)
|
||||
|
||||
onRenderActiveChanged: transientSurfaceTracker?.setActive(root, renderActive, contextWindow)
|
||||
Component.onDestruction: transientSurfaceTracker?.unregister(root)
|
||||
|
||||
Connections {
|
||||
target: root.transientSurfaceTracker
|
||||
ignoreUnknownSignals: true
|
||||
|
||||
function onCloseRequested() {
|
||||
root.hide();
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: menuTextMetrics
|
||||
text: root.longestMenuText
|
||||
@@ -531,8 +518,8 @@ Item {
|
||||
height: root.effectiveMenuHeight
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
|
||||
border.width: BlurService.enabled ? BlurService.borderWidth : 1
|
||||
opacity: root.openState ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
|
||||
@@ -15,7 +15,6 @@ Item {
|
||||
property var _visualRows: []
|
||||
property var _flatIndexToRowMap: ({})
|
||||
property var _cumulativeHeights: []
|
||||
property var transientSurfaceTracker: null
|
||||
readonly property bool _bottomSectionHeaderActive: leadingSectionHeaderAtBottom && (controller?.sections?.length ?? 0) > 0
|
||||
|
||||
signal itemRightClicked(int index, var item, real mouseX, real mouseY)
|
||||
@@ -260,7 +259,6 @@ Item {
|
||||
return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
|
||||
}
|
||||
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
|
||||
transientSurfaceTracker: root.transientSurfaceTracker
|
||||
}
|
||||
|
||||
ResultItem {
|
||||
@@ -449,7 +447,6 @@ Item {
|
||||
return root.controller?.canCollapseSection(stickyHeader.stickyHeaderSection?.id) ?? false;
|
||||
}
|
||||
isSticky: true
|
||||
transientSurfaceTracker: root.transientSurfaceTracker
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +472,6 @@ Item {
|
||||
canCollapse: root.controller?.canCollapseSection(section?.id ?? "") ?? false
|
||||
isSticky: true
|
||||
popupAbove: true
|
||||
transientSurfaceTracker: root.transientSurfaceTracker
|
||||
}
|
||||
|
||||
Item {
|
||||
|
||||
@@ -17,21 +17,9 @@ Rectangle {
|
||||
property bool isSticky: false
|
||||
property bool popupAbove: false
|
||||
property Item popupAboveItem: null
|
||||
property var transientSurfaceTracker: null
|
||||
|
||||
signal viewModeToggled
|
||||
|
||||
Component.onDestruction: transientSurfaceTracker?.unregister(root)
|
||||
|
||||
Connections {
|
||||
target: root.transientSurfaceTracker
|
||||
ignoreUnknownSignals: true
|
||||
|
||||
function onCloseRequested() {
|
||||
categoryPopup.close();
|
||||
}
|
||||
}
|
||||
|
||||
width: parent?.width ?? 200
|
||||
height: 32
|
||||
color: isSticky ? Theme.withAlpha(Theme.surfaceHover, 0) : (hoverArea.containsMouse ? Theme.surfaceHover : Theme.withAlpha(Theme.surfaceHover, 0))
|
||||
@@ -154,8 +142,6 @@ Rectangle {
|
||||
dim: false
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
onVisibleChanged: root.transientSurfaceTracker?.setActive(root, visible, null)
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -12,7 +13,6 @@ FocusScope {
|
||||
property alias searchField: searchInput
|
||||
property alias controller: searchController
|
||||
readonly property alias activeContextMenu: contextMenu
|
||||
property var transientSurfaceTracker: null
|
||||
|
||||
readonly property bool _hasQuery: searchInput.text.length > 0
|
||||
readonly property real _searchBarH: 56
|
||||
@@ -49,7 +49,7 @@ FocusScope {
|
||||
}
|
||||
|
||||
function closeTransientUi() {
|
||||
transientSurfaceTracker?.closeAll?.();
|
||||
contextMenu.hide();
|
||||
root.enabled = true;
|
||||
}
|
||||
|
||||
@@ -221,7 +221,6 @@ FocusScope {
|
||||
searchField: searchInput
|
||||
parentHandler: root
|
||||
allowEditActions: false
|
||||
transientSurfaceTracker: root.transientSurfaceTracker
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -320,13 +319,13 @@ FocusScope {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: height / 2
|
||||
color: chipColor.value
|
||||
color: categoryChip.isSelected ? Theme.primary : chipArea.containsMouse ? Theme.surfaceHover : Theme.surfaceVariantAlpha
|
||||
|
||||
DankColorAnimation {
|
||||
id: chipColor
|
||||
to: categoryChip.isSelected ? Theme.primary : chipArea.containsMouse ? Theme.surfaceHover : Theme.surfaceVariantAlpha
|
||||
duration: root._fastDuration
|
||||
easingType: Theme.standardEasing
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: root._fastDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
|
||||
@@ -89,8 +89,8 @@ Popup {
|
||||
contentItem: Rectangle {
|
||||
color: Theme.floatingSurface
|
||||
radius: Theme.cornerRadius
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
|
||||
border.width: BlurService.enabled ? BlurService.borderWidth : 1
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
|
||||
@@ -191,21 +191,21 @@ FloatingWindow {
|
||||
width: isActive ? dotSize * 3 : dotSize
|
||||
height: dotSize
|
||||
radius: dotSize / 2
|
||||
color: dotColor.value
|
||||
color: isActive ? Theme.primary : Theme.surfaceTextAlpha
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankColorAnimation {
|
||||
id: dotColor
|
||||
to: isActive ? Theme.primary : Theme.surfaceTextAlpha
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ DankModal {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Create a new %1 session (^N)").arg(MuxService.displayName)
|
||||
text: I18n.tr("Create a new %1 session (n)").arg(MuxService.displayName)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
@@ -549,7 +549,7 @@ DankModal {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: muxModal.searchText.length > 0 ? I18n.tr("Try a different search") : I18n.tr("Press Ctrl+N or click 'New Session' to create one")
|
||||
text: muxModal.searchText.length > 0 ? I18n.tr("Try a different search") : I18n.tr("Press 'n' or click 'New Session' to create one")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
@@ -210,14 +210,12 @@ DankModal {
|
||||
NotificationHeader {
|
||||
id: notificationHeader
|
||||
keyboardController: modalKeyboardController
|
||||
transientSurfaceTracker: notificationModal.transientSurfaceTracker
|
||||
onCurrentTabChanged: notificationModal.currentTab = currentTab
|
||||
Component.onCompleted: notificationModal.notificationHeaderRef = notificationHeader
|
||||
}
|
||||
|
||||
NotificationSettings {
|
||||
id: notificationSettings
|
||||
transientSurfaceTracker: notificationModal.transientSurfaceTracker
|
||||
expanded: notificationHeader.showSettings
|
||||
}
|
||||
|
||||
@@ -227,7 +225,6 @@ DankModal {
|
||||
height: parent.height - y
|
||||
visible: notificationHeader.currentTab === 0
|
||||
keyboardController: modalKeyboardController
|
||||
transientSurfaceTracker: notificationModal.transientSurfaceTracker
|
||||
Component.onCompleted: {
|
||||
notificationModal.notificationListRef = notificationList;
|
||||
if (modalKeyboardController) {
|
||||
|
||||
@@ -26,8 +26,6 @@ DankModal {
|
||||
property int holdActionIndex: -1
|
||||
property real holdProgress: 0
|
||||
property bool showHoldHint: false
|
||||
property bool holdFromKeyboard: false
|
||||
property string pendingKeyAction: ""
|
||||
|
||||
readonly property bool needsConfirmation: SettingsData.powerActionConfirm
|
||||
readonly property int holdDurationMs: SettingsData.powerActionHoldDuration * 1000
|
||||
@@ -41,10 +39,6 @@ DankModal {
|
||||
|
||||
function startHold(action, actionIndex) {
|
||||
if (!needsConfirmation || !actionNeedsConfirm(action)) {
|
||||
if (holdFromKeyboard) {
|
||||
pendingKeyAction = action;
|
||||
return;
|
||||
}
|
||||
executeAction(action);
|
||||
return;
|
||||
}
|
||||
@@ -56,7 +50,6 @@ DankModal {
|
||||
}
|
||||
|
||||
function cancelHold() {
|
||||
pendingKeyAction = "";
|
||||
if (holdAction === "")
|
||||
return;
|
||||
const wasHolding = holdProgress > 0;
|
||||
@@ -75,12 +68,8 @@ DankModal {
|
||||
cancelHold();
|
||||
return;
|
||||
}
|
||||
holdTimer.stop();
|
||||
if (holdFromKeyboard) {
|
||||
pendingKeyAction = holdAction;
|
||||
return;
|
||||
}
|
||||
const action = holdAction;
|
||||
holdTimer.stop();
|
||||
holdAction = "";
|
||||
holdActionIndex = -1;
|
||||
holdProgress = 0;
|
||||
@@ -285,8 +274,6 @@ DankModal {
|
||||
holdActionIndex = -1;
|
||||
holdProgress = 0;
|
||||
showHoldHint = false;
|
||||
holdFromKeyboard = false;
|
||||
pendingKeyAction = "";
|
||||
updateVisibleActions();
|
||||
const defaultIndex = getDefaultActionIndex();
|
||||
selectedIndex = defaultIndex;
|
||||
@@ -309,7 +296,6 @@ DankModal {
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
holdFromKeyboard = true;
|
||||
if (SettingsData.powerMenuGridLayout) {
|
||||
handleGridNavigation(event, true);
|
||||
} else {
|
||||
@@ -331,16 +317,7 @@ DankModal {
|
||||
function handleListNavigation(event, isPressed) {
|
||||
if (!isPressed) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
|
||||
if (pendingKeyAction !== "") {
|
||||
const action = pendingKeyAction;
|
||||
pendingKeyAction = "";
|
||||
holdAction = "";
|
||||
holdActionIndex = -1;
|
||||
holdProgress = 0;
|
||||
executeAction(action);
|
||||
} else {
|
||||
cancelHold();
|
||||
}
|
||||
cancelHold();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
@@ -434,16 +411,7 @@ DankModal {
|
||||
function handleGridNavigation(event, isPressed) {
|
||||
if (!isPressed) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
|
||||
if (pendingKeyAction !== "") {
|
||||
const action = pendingKeyAction;
|
||||
pendingKeyAction = "";
|
||||
holdAction = "";
|
||||
holdActionIndex = -1;
|
||||
holdProgress = 0;
|
||||
executeAction(action);
|
||||
} else {
|
||||
cancelHold();
|
||||
}
|
||||
cancelHold();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
@@ -677,7 +645,6 @@ DankModal {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
root.holdFromKeyboard = false;
|
||||
root.selectedRow = Math.floor(index / root.gridColumns);
|
||||
root.selectedCol = index % root.gridColumns;
|
||||
root.selectedIndex = index;
|
||||
@@ -825,7 +792,6 @@ DankModal {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: {
|
||||
root.holdFromKeyboard = false;
|
||||
root.selectedIndex = index;
|
||||
root.startHold(modelData, index);
|
||||
}
|
||||
|
||||
@@ -122,7 +122,6 @@ DankPopout {
|
||||
anchors.fill: parent
|
||||
parentModal: modalAdapter
|
||||
viewModeContext: "appDrawer"
|
||||
transientSurfaceTracker: appDrawerPopout.transientSurfaceTracker
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
|
||||
@@ -393,10 +393,6 @@ Item {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
if (!hist || hist.length < 2)
|
||||
return;
|
||||
var r = Math.max(0, tile.radius);
|
||||
ctx.beginPath();
|
||||
ctx.roundedRect(0, 0, width, height, r, r);
|
||||
ctx.clip();
|
||||
var maxVal = 100;
|
||||
if (tile.tileType === "net" || tile.tileType === "disk") {
|
||||
maxVal = 1;
|
||||
|
||||
@@ -314,7 +314,6 @@ DankPopout {
|
||||
id: bluetoothDetailComponent
|
||||
BluetoothDetail {
|
||||
id: bluetoothDetail
|
||||
bluetoothCodecModalRef: contentLoader.item ? contentLoader.item.bluetoothCodecSelector : null
|
||||
onShowCodecSelector: function (device) {
|
||||
if (contentLoader.item && contentLoader.item.bluetoothCodecSelector) {
|
||||
contentLoader.item.bluetoothCodecSelector.show(device);
|
||||
|
||||
@@ -15,8 +15,6 @@ Item {
|
||||
property var availableCodecs: []
|
||||
property string currentCodec: ""
|
||||
property bool isLoading: false
|
||||
property string statusMessage: ""
|
||||
property bool statusIsError: false
|
||||
|
||||
readonly property bool deviceValid: device !== null && device.connected && BluetoothService.isAudioDevice(device)
|
||||
|
||||
@@ -31,8 +29,6 @@ Item {
|
||||
isLoading = true;
|
||||
availableCodecs = [];
|
||||
currentCodec = "";
|
||||
statusMessage = "";
|
||||
statusIsError = false;
|
||||
visible = true;
|
||||
modalVisible = true;
|
||||
queryCodecs();
|
||||
@@ -64,16 +60,6 @@ Item {
|
||||
availableCodecs = codecs;
|
||||
currentCodec = current;
|
||||
isLoading = false;
|
||||
if (BluetoothService.pactlChecked && !BluetoothService.pactlAvailable) {
|
||||
statusMessage = I18n.tr("Codec switching is unavailable because pactl was not found");
|
||||
statusIsError = true;
|
||||
} else if (codecs.length === 0) {
|
||||
statusMessage = I18n.tr("No codecs found");
|
||||
statusIsError = false;
|
||||
} else {
|
||||
statusMessage = "";
|
||||
statusIsError = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,7 +123,7 @@ Item {
|
||||
Rectangle {
|
||||
id: modalBackground
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, BlurService.enabled ? 0.72 : 0.5)
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
opacity: modalVisible ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
@@ -155,7 +141,7 @@ Item {
|
||||
focus: root.visible
|
||||
enabled: root.visible
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
Keys.onEscapePressed: {
|
||||
root.hide();
|
||||
event.accepted = true;
|
||||
}
|
||||
@@ -167,9 +153,9 @@ Item {
|
||||
width: 320
|
||||
height: contentColumn.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, BlurService.enabled ? 0.96 : Theme.popupTransparency)
|
||||
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
|
||||
border.width: BlurService.enabled ? BlurService.borderWidth : Theme.layerOutlineWidth
|
||||
color: Theme.floatingSurface
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: Theme.layerOutlineWidth
|
||||
opacity: modalVisible ? 1 : 0
|
||||
scale: modalVisible ? 1 : 0.9
|
||||
|
||||
@@ -235,24 +221,16 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (isLoading)
|
||||
return I18n.tr("Loading codecs...");
|
||||
if (statusMessage.length > 0)
|
||||
return statusMessage;
|
||||
return I18n.tr("Current: %1").arg(currentCodec);
|
||||
}
|
||||
text: isLoading ? I18n.tr("Loading codecs...") : I18n.tr("Current: %1").arg(currentCodec)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: statusIsError ? Theme.error : (isLoading ? Theme.primary : Theme.surfaceTextMedium)
|
||||
color: isLoading ? Theme.primary : Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
visible: !isLoading && availableCodecs.length > 0
|
||||
visible: !isLoading
|
||||
|
||||
Repeater {
|
||||
model: availableCodecs
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user