mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-16 16:15:23 -04:00
feat(Hyprland): Introduce Lua support for Hyprland configurations
- Note: We do not convert your existing conf configs to lua. This update only reflects DMS defaults state - Updated README.md to reflect changes - Updated Keyboard shortcut support
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
@@ -37,6 +38,7 @@ var runCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.ApplyEnvOverrides()
|
log.ApplyEnvOverrides()
|
||||||
|
config.CleanupStrayHyprlandConfFile(log.Infof)
|
||||||
if daemon {
|
if daemon {
|
||||||
runShellDaemon(session)
|
runShellDaemon(session)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/luaconfig"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -27,7 +28,21 @@ var resolveIncludeCmd = &cobra.Command{
|
|||||||
case 0:
|
case 0:
|
||||||
return []string{"hyprland", "niri", "mangowc"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri", "mangowc"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
case 1:
|
case 1:
|
||||||
return []string{"cursor.kdl", "cursor.conf", "outputs.kdl", "outputs.conf", "binds.kdl", "binds.conf"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{
|
||||||
|
"binds.lua",
|
||||||
|
"binds-user.lua",
|
||||||
|
"colors.lua",
|
||||||
|
"layout.lua",
|
||||||
|
"outputs.lua",
|
||||||
|
"cursor.lua",
|
||||||
|
"windowrules.lua",
|
||||||
|
"cursor.kdl",
|
||||||
|
"outputs.kdl",
|
||||||
|
"binds.kdl",
|
||||||
|
"cursor.conf",
|
||||||
|
"outputs.conf",
|
||||||
|
"binds.conf",
|
||||||
|
}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -82,17 +97,35 @@ func checkHyprlandInclude(filename string) (IncludeResult, error) {
|
|||||||
result.Exists = true
|
result.Exists = true
|
||||||
}
|
}
|
||||||
|
|
||||||
mainConfig := filepath.Join(configDir, "hyprland.conf")
|
targetAbs, err := filepath.Abs(targetPath)
|
||||||
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
if err != nil {
|
||||||
return result, nil
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetRel := filepath.ToSlash(filepath.Join("dms", filename))
|
||||||
|
|
||||||
|
mainLua := filepath.Join(configDir, "hyprland.lua")
|
||||||
|
if _, err := os.Stat(mainLua); err == nil {
|
||||||
|
processedLua := make(map[string]bool)
|
||||||
|
if luaconfig.RequiresTarget(mainLua, targetAbs, processedLua) {
|
||||||
|
result.Included = true
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConf := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
if _, err := os.Stat(mainConf); err == nil {
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
if hyprlandFindIncludeHyprlang(mainConf, targetRel, processed) {
|
||||||
|
result.Included = true
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processed := make(map[string]bool)
|
|
||||||
result.Included = hyprlandFindInclude(mainConfig, "dms/"+filename, processed)
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func hyprlandFindInclude(filePath, target string, processed map[string]bool) bool {
|
func hyprlandFindIncludeHyprlang(filePath, target string, processed map[string]bool) bool {
|
||||||
absPath, err := filepath.Abs(filePath)
|
absPath, err := filepath.Abs(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
@@ -141,7 +174,7 @@ func hyprlandFindInclude(filePath, target string, processed map[string]bool) boo
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if hyprlandFindInclude(expanded, target, processed) {
|
if hyprlandFindIncludeHyprlang(expanded, target, processed) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,12 +51,20 @@ var keybindsSetCmd = &cobra.Command{
|
|||||||
|
|
||||||
var keybindsRemoveCmd = &cobra.Command{
|
var keybindsRemoveCmd = &cobra.Command{
|
||||||
Use: "remove <provider> <key>",
|
Use: "remove <provider> <key>",
|
||||||
Short: "Remove a keybind override",
|
Short: "Remove a keybind",
|
||||||
Long: "Remove a keybind override from the specified provider",
|
Long: "Remove a keybind. For Hyprland this writes a negative override to dms/binds-user.lua so the key stays unbound across DMS updates. For other providers it deletes the entry from the managed file.",
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
Run: runKeybindsRemove,
|
Run: runKeybindsRemove,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var keybindsResetCmd = &cobra.Command{
|
||||||
|
Use: "reset <provider> <key>",
|
||||||
|
Short: "Reset a keybind override to its DMS default",
|
||||||
|
Long: "Drop the user override for the given key so the DMS default re-applies. For providers without a separate default file (Niri, MangoWC) this is equivalent to remove.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
Run: runKeybindsReset,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
|
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
|
||||||
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
||||||
@@ -72,6 +80,7 @@ func init() {
|
|||||||
keybindsCmd.AddCommand(keybindsShowCmd)
|
keybindsCmd.AddCommand(keybindsShowCmd)
|
||||||
keybindsCmd.AddCommand(keybindsSetCmd)
|
keybindsCmd.AddCommand(keybindsSetCmd)
|
||||||
keybindsCmd.AddCommand(keybindsRemoveCmd)
|
keybindsCmd.AddCommand(keybindsRemoveCmd)
|
||||||
|
keybindsCmd.AddCommand(keybindsResetCmd)
|
||||||
|
|
||||||
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
|
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
|
||||||
return providers.NewJSONFileProvider(filePath)
|
return providers.NewJSONFileProvider(filePath)
|
||||||
@@ -263,3 +272,19 @@ func runKeybindsRemove(_ *cobra.Command, args []string) {
|
|||||||
}, "", " ")
|
}, "", " ")
|
||||||
fmt.Fprintln(os.Stdout, string(output))
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runKeybindsReset(_ *cobra.Command, args []string) {
|
||||||
|
providerName, key := args[0], args[1]
|
||||||
|
writable := getWritableProvider(providerName)
|
||||||
|
|
||||||
|
if err := writable.ResetBind(key); err != nil {
|
||||||
|
log.Fatalf("Error resetting keybind: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := json.MarshalIndent(map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"key": key,
|
||||||
|
"reset": true,
|
||||||
|
}, "", " ")
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|||||||
@@ -109,25 +109,25 @@ type dmsConfigSpec struct {
|
|||||||
var dmsConfigSpecs = map[string]dmsConfigSpec{
|
var dmsConfigSpecs = map[string]dmsConfigSpec{
|
||||||
"binds": {
|
"binds": {
|
||||||
niriFile: "binds.kdl",
|
niriFile: "binds.kdl",
|
||||||
hyprFile: "binds.conf",
|
hyprFile: "binds.lua",
|
||||||
niriContent: func(t string) string {
|
niriContent: func(t string) string {
|
||||||
return strings.ReplaceAll(config.NiriBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
return strings.ReplaceAll(config.NiriBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
||||||
},
|
},
|
||||||
hyprContent: func(t string) string {
|
hyprContent: func(t string) string {
|
||||||
return strings.ReplaceAll(config.HyprBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
return strings.ReplaceAll(config.DMSBindsLuaConfig, "{{TERMINAL_COMMAND}}", t)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
niriFile: "layout.kdl",
|
niriFile: "layout.kdl",
|
||||||
hyprFile: "layout.conf",
|
hyprFile: "layout.lua",
|
||||||
niriContent: func(_ string) string { return config.NiriLayoutConfig },
|
niriContent: func(_ string) string { return config.NiriLayoutConfig },
|
||||||
hyprContent: func(_ string) string { return config.HyprLayoutConfig },
|
hyprContent: func(_ string) string { return config.DMSLayoutLuaConfig },
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
niriFile: "colors.kdl",
|
niriFile: "colors.kdl",
|
||||||
hyprFile: "colors.conf",
|
hyprFile: "colors.lua",
|
||||||
niriContent: func(_ string) string { return config.NiriColorsConfig },
|
niriContent: func(_ string) string { return config.NiriColorsConfig },
|
||||||
hyprContent: func(_ string) string { return config.HyprColorsConfig },
|
hyprContent: func(_ string) string { return config.DMSColorsLuaConfig },
|
||||||
},
|
},
|
||||||
"alttab": {
|
"alttab": {
|
||||||
niriFile: "alttab.kdl",
|
niriFile: "alttab.kdl",
|
||||||
@@ -135,21 +135,21 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{
|
|||||||
},
|
},
|
||||||
"outputs": {
|
"outputs": {
|
||||||
niriFile: "outputs.kdl",
|
niriFile: "outputs.kdl",
|
||||||
hyprFile: "outputs.conf",
|
hyprFile: "outputs.lua",
|
||||||
niriContent: func(_ string) string { return "" },
|
niriContent: func(_ string) string { return "" },
|
||||||
hyprContent: func(_ string) string { return "" },
|
hyprContent: func(_ string) string { return config.DMSOutputsLuaConfig },
|
||||||
},
|
},
|
||||||
"cursor": {
|
"cursor": {
|
||||||
niriFile: "cursor.kdl",
|
niriFile: "cursor.kdl",
|
||||||
hyprFile: "cursor.conf",
|
hyprFile: "cursor.lua",
|
||||||
niriContent: func(_ string) string { return "" },
|
niriContent: func(_ string) string { return "" },
|
||||||
hyprContent: func(_ string) string { return "" },
|
hyprContent: func(_ string) string { return config.DMSCursorLuaConfig },
|
||||||
},
|
},
|
||||||
"windowrules": {
|
"windowrules": {
|
||||||
niriFile: "windowrules.kdl",
|
niriFile: "windowrules.kdl",
|
||||||
hyprFile: "windowrules.conf",
|
hyprFile: "windowrules.lua",
|
||||||
niriContent: func(_ string) string { return "" },
|
niriContent: func(_ string) string { return "" },
|
||||||
hyprContent: func(_ string) string { return "" },
|
hyprContent: func(_ string) string { return config.DMSWindowRulesLuaConfig },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,16 +438,22 @@ func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.
|
|||||||
willBackup := false
|
willBackup := false
|
||||||
|
|
||||||
if wmSelected {
|
if wmSelected {
|
||||||
var configPath string
|
var configPaths []string
|
||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
configPath = filepath.Join(homeDir, ".config", "niri", "config.kdl")
|
configPaths = []string{filepath.Join(homeDir, ".config", "niri", "config.kdl")}
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
configPath = filepath.Join(homeDir, ".config", "hypr", "hyprland.conf")
|
configPaths = []string{
|
||||||
|
filepath.Join(homeDir, ".config", "hypr", "hyprland.lua"),
|
||||||
|
filepath.Join(homeDir, ".config", "hypr", "hyprland.conf"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(configPath); err == nil {
|
for _, configPath := range configPaths {
|
||||||
willBackup = true
|
if _, err := os.Stat(configPath); err == nil {
|
||||||
|
willBackup = true
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ var windowrulesListCmd = &cobra.Command{
|
|||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -40,8 +40,7 @@ var windowrulesAddCmd = &cobra.Command{
|
|||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
// ! disabled hyprland return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -55,7 +54,7 @@ var windowrulesUpdateCmd = &cobra.Command{
|
|||||||
Args: cobra.ExactArgs(3),
|
Args: cobra.ExactArgs(3),
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -69,7 +68,7 @@ var windowrulesRemoveCmd = &cobra.Command{
|
|||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -83,7 +82,7 @@ var windowrulesReorderCmd = &cobra.Command{
|
|||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -118,9 +117,9 @@ func getCompositor(args []string) string {
|
|||||||
if os.Getenv("NIRI_SOCKET") != "" {
|
if os.Getenv("NIRI_SOCKET") != "" {
|
||||||
return "niri"
|
return "niri"
|
||||||
}
|
}
|
||||||
// if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
||||||
// return "hyprland"
|
return "hyprland"
|
||||||
// }
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +182,6 @@ func runWindowrulesList(cmd *cobra.Command, args []string) {
|
|||||||
result.DMSStatus = parseResult.DMSStatus
|
result.DMSStatus = parseResult.DMSStatus
|
||||||
|
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
log.Fatalf("Hyprland support is currently disabled.") // ! disabled hyprland
|
|
||||||
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to expand hyprland config path: %v", err)
|
log.Fatalf("Failed to expand hyprland config path: %v", err)
|
||||||
|
|||||||
+179
-110
@@ -12,6 +12,8 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const hyprlandBackupDirName = ".dms-backups"
|
||||||
|
|
||||||
type ConfigDeployer struct {
|
type ConfigDeployer struct {
|
||||||
logChan chan<- string
|
logChan chan<- string
|
||||||
}
|
}
|
||||||
@@ -63,12 +65,23 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
|
|||||||
var results []DeploymentResult
|
var results []DeploymentResult
|
||||||
|
|
||||||
// Primary config file paths used to detect fresh installs.
|
// Primary config file paths used to detect fresh installs.
|
||||||
configPrimaryPaths := map[string]string{
|
configPrimaryPaths := map[string][]string{
|
||||||
"Niri": filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
|
"Niri": {
|
||||||
"Hyprland": filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
|
||||||
"Ghostty": filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"),
|
},
|
||||||
"Kitty": filepath.Join(os.Getenv("HOME"), ".config", "kitty", "kitty.conf"),
|
"Hyprland": {
|
||||||
"Alacritty": filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "alacritty.toml"),
|
filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.lua"),
|
||||||
|
filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
||||||
|
},
|
||||||
|
"Ghostty": {
|
||||||
|
filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"),
|
||||||
|
},
|
||||||
|
"Kitty": {
|
||||||
|
filepath.Join(os.Getenv("HOME"), ".config", "kitty", "kitty.conf"),
|
||||||
|
},
|
||||||
|
"Alacritty": {
|
||||||
|
filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "alacritty.toml"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldReplaceConfig := func(configType string) bool {
|
shouldReplaceConfig := func(configType string) bool {
|
||||||
@@ -81,8 +94,15 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
|
|||||||
}
|
}
|
||||||
// Config is explicitly set to "don't replace" — but still deploy
|
// Config is explicitly set to "don't replace" — but still deploy
|
||||||
// if the config file doesn't exist yet (fresh install scenario).
|
// if the config file doesn't exist yet (fresh install scenario).
|
||||||
if primaryPath, ok := configPrimaryPaths[configType]; ok {
|
if primaryPaths, ok := configPrimaryPaths[configType]; ok {
|
||||||
if _, err := os.Stat(primaryPath); os.IsNotExist(err) {
|
exists := false
|
||||||
|
for _, primaryPath := range primaryPaths {
|
||||||
|
if _, err := os.Stat(primaryPath); err == nil {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,7 +515,7 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dms
|
|||||||
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
|
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
|
||||||
result := DeploymentResult{
|
result := DeploymentResult{
|
||||||
ConfigType: "Hyprland",
|
ConfigType: "Hyprland",
|
||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.lua"),
|
||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(result.Path)
|
configDir := filepath.Dir(result.Path)
|
||||||
@@ -510,20 +530,20 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
|
backupDir := filepath.Join(configDir, hyprlandBackupDirName, timestamp)
|
||||||
var existingConfig string
|
var existingConfig string
|
||||||
if _, err := os.Stat(result.Path); err == nil {
|
existingData, existingPath, err := readExistingHyprlandConfig(configDir)
|
||||||
cd.log("Found existing Hyprland configuration")
|
if err != nil {
|
||||||
|
result.Error = err
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
if existingData != "" {
|
||||||
|
existingConfig = existingData
|
||||||
|
cd.log(fmt.Sprintf("Found existing Hyprland configuration at %s", existingPath))
|
||||||
|
|
||||||
existingData, err := os.ReadFile(result.Path)
|
result.BackupPath = filepath.Join(backupDir, filepath.Base(existingPath))
|
||||||
if err != nil {
|
if err := backupHyprlandConfigFile(existingPath, result.BackupPath, []byte(existingData), strings.EqualFold(filepath.Ext(existingPath), ".conf")); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to read existing config: %w", err)
|
|
||||||
return result, result.Error
|
|
||||||
}
|
|
||||||
existingConfig = string(existingData)
|
|
||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
|
||||||
result.BackupPath = result.Path + ".backup." + timestamp
|
|
||||||
if err := os.WriteFile(result.BackupPath, existingData, 0o644); err != nil {
|
|
||||||
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -542,10 +562,10 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
terminalCommand = "ghostty"
|
terminalCommand = "ghostty"
|
||||||
}
|
}
|
||||||
|
|
||||||
newConfig := strings.ReplaceAll(HyprlandConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
newConfig := strings.ReplaceAll(HyprlandLuaConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||||
|
|
||||||
if !useSystemd {
|
if !useSystemd {
|
||||||
newConfig = cd.transformHyprlandConfigForNonSystemd(newConfig, terminalCommand)
|
newConfig = transformHyprlandLuaForNonSystemd(newConfig, terminalCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
if existingConfig != "" {
|
if existingConfig != "" {
|
||||||
@@ -563,6 +583,18 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
movedLegacy, err := backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to back up legacy hyprlang configs: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
if movedLegacy > 0 {
|
||||||
|
if result.BackupPath == "" {
|
||||||
|
result.BackupPath = backupDir
|
||||||
|
}
|
||||||
|
cd.log(fmt.Sprintf("Moved %d legacy hyprlang config(s) to %s", movedLegacy, backupDir))
|
||||||
|
}
|
||||||
|
|
||||||
if err := cd.deployHyprlandDmsConfigs(dmsDir, terminalCommand); err != nil {
|
if err := cd.deployHyprlandDmsConfigs(dmsDir, terminalCommand); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
@@ -573,29 +605,118 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func backupHyprlandConfigFile(src, dst string, data []byte, removeSource bool) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(dst, data, 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if removeSource {
|
||||||
|
if err := os.Remove(src); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir string) (int, error) {
|
||||||
|
legacyPaths := []string{filepath.Join(configDir, "hyprland.conf")}
|
||||||
|
dmsConfPaths, err := filepath.Glob(filepath.Join(dmsDir, "*.conf"))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
legacyPaths = append(legacyPaths, dmsConfPaths...)
|
||||||
|
backupPaths, err := adjacentHyprlandBackupFiles(configDir, dmsDir)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
legacyPaths = append(legacyPaths, backupPaths...)
|
||||||
|
|
||||||
|
moved := 0
|
||||||
|
for _, src := range legacyPaths {
|
||||||
|
info, err := os.Lstat(src)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return moved, err
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rel, err := filepath.Rel(configDir, src)
|
||||||
|
if err != nil {
|
||||||
|
rel = filepath.Base(src)
|
||||||
|
}
|
||||||
|
dst := filepath.Join(backupDir, rel)
|
||||||
|
if err := moveHyprlandConfigFile(src, dst); err != nil {
|
||||||
|
return moved, err
|
||||||
|
}
|
||||||
|
moved++
|
||||||
|
}
|
||||||
|
|
||||||
|
return moved, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveHyprlandConfigFile(src, dst string) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func adjacentHyprlandBackupFiles(configDir, dmsDir string) ([]string, error) {
|
||||||
|
var paths []string
|
||||||
|
patterns := []string{
|
||||||
|
filepath.Join(configDir, "hyprland.conf.backup.*"),
|
||||||
|
filepath.Join(configDir, "hyprland.lua.backup.*"),
|
||||||
|
filepath.Join(dmsDir, "*.conf.backup.*"),
|
||||||
|
filepath.Join(dmsDir, "*.lua.backup.*"),
|
||||||
|
}
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
matches, err := filepath.Glob(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
paths = append(paths, matches...)
|
||||||
|
}
|
||||||
|
return paths, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalCommand string) error {
|
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalCommand string) error {
|
||||||
configs := []struct {
|
configs := []struct {
|
||||||
name string
|
name string
|
||||||
content string
|
content string
|
||||||
|
overwrite bool
|
||||||
}{
|
}{
|
||||||
{"colors.conf", HyprColorsConfig},
|
{name: "colors.lua", content: DMSColorsLuaConfig},
|
||||||
{"layout.conf", HyprLayoutConfig},
|
{name: "layout.lua", content: DMSLayoutLuaConfig},
|
||||||
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
{name: "binds.lua", content: strings.ReplaceAll(DMSBindsLuaConfig, "{{TERMINAL_COMMAND}}", terminalCommand), overwrite: true},
|
||||||
{"outputs.conf", ""},
|
{name: "binds-user.lua", content: DMSBindsUserLuaConfig},
|
||||||
{"cursor.conf", ""},
|
{name: "outputs.lua", content: DMSOutputsLuaConfig},
|
||||||
{"windowrules.conf", ""},
|
{name: "cursor.lua", content: DMSCursorLuaConfig},
|
||||||
|
{name: "windowrules.lua", content: DMSWindowRulesLuaConfig},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
path := filepath.Join(dmsDir, cfg.name)
|
path := filepath.Join(dmsDir, cfg.name)
|
||||||
// Skip if file already exists and is not empty to preserve user modifications
|
existed := false
|
||||||
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||||
|
existed = true
|
||||||
|
}
|
||||||
|
if existed && !cfg.overwrite {
|
||||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
|
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
||||||
}
|
}
|
||||||
|
if existed {
|
||||||
|
cd.log(fmt.Sprintf("Updated %s", cfg.name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,94 +724,42 @@ func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalComman
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir string) (string, error) {
|
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir string) (string, error) {
|
||||||
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
|
_ = newConfig
|
||||||
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
|
lines := extractHyprlangMonitorLines(existingConfig)
|
||||||
|
if len(lines) == 0 {
|
||||||
if len(existingMonitors) == 0 {
|
|
||||||
return newConfig, nil
|
return newConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
outputsPath := filepath.Join(dmsDir, "outputs.conf")
|
outputsPath := filepath.Join(dmsDir, "outputs.lua")
|
||||||
if _, err := os.Stat(outputsPath); err != nil {
|
if info, err := os.Stat(outputsPath); err == nil && info.Size() > 0 {
|
||||||
var outputsContent strings.Builder
|
cd.log("Skipping monitor migration: dms/outputs.lua already exists")
|
||||||
for _, monitor := range existingMonitors {
|
return newConfig, nil
|
||||||
outputsContent.WriteString(monitor)
|
|
||||||
outputsContent.WriteString("\n")
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0o644); err != nil {
|
|
||||||
cd.log(fmt.Sprintf("Warning: Failed to migrate monitors to %s: %v", outputsPath, err))
|
|
||||||
} else {
|
|
||||||
cd.log("Migrated monitor sections to dms/outputs.conf")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
|
var b strings.Builder
|
||||||
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
|
b.WriteString("-- Migrated from existing hyprlang monitor lines\n\n")
|
||||||
|
ok := 0
|
||||||
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
|
|
||||||
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
|
|
||||||
|
|
||||||
if headerMatch == nil {
|
|
||||||
return "", fmt.Errorf("could not find MONITOR CONFIG section")
|
|
||||||
}
|
|
||||||
|
|
||||||
insertPos := headerMatch[1] + 1
|
|
||||||
|
|
||||||
var builder strings.Builder
|
|
||||||
builder.WriteString(mergedConfig[:insertPos])
|
|
||||||
builder.WriteString("# Monitors from existing configuration\n")
|
|
||||||
|
|
||||||
for _, monitor := range existingMonitors {
|
|
||||||
builder.WriteString(monitor)
|
|
||||||
builder.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.WriteString(mergedConfig[insertPos:])
|
|
||||||
|
|
||||||
return builder.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalCommand string) string {
|
|
||||||
lines := strings.Split(config, "\n")
|
|
||||||
var result []string
|
|
||||||
startupSectionFound := false
|
|
||||||
|
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
trimmed := strings.TrimSpace(line)
|
lua, err := hyprlangMonitorLineToLua(line)
|
||||||
if strings.HasPrefix(trimmed, "exec-once = dbus-update-activation-environment") {
|
if err != nil {
|
||||||
|
cd.log(fmt.Sprintf("Warning: could not migrate monitor line %q: %v", line, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
|
b.WriteString(lua)
|
||||||
startupSectionFound = true
|
b.WriteByte('\n')
|
||||||
result = append(result, "exec-once = dms run")
|
ok++
|
||||||
result = append(result, "env = QT_QPA_PLATFORM,wayland;xcb")
|
|
||||||
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
|
|
||||||
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
|
|
||||||
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
|
|
||||||
result = append(result, fmt.Sprintf("env = TERMINAL,%s", terminalCommand))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result = append(result, line)
|
|
||||||
}
|
}
|
||||||
|
if ok == 0 {
|
||||||
if !startupSectionFound {
|
return newConfig, nil
|
||||||
for i, line := range result {
|
|
||||||
if strings.Contains(line, "STARTUP APPS") {
|
|
||||||
insertLines := []string{
|
|
||||||
"exec-once = dms run",
|
|
||||||
"env = QT_QPA_PLATFORM,wayland;xcb",
|
|
||||||
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
|
|
||||||
"env = QT_QPA_PLATFORMTHEME,gtk3",
|
|
||||||
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
|
|
||||||
fmt.Sprintf("env = TERMINAL,%s", terminalCommand),
|
|
||||||
}
|
|
||||||
result = append(result[:i+2], append(insertLines, result[i+2:]...)...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
b.WriteByte('\n')
|
||||||
return strings.Join(result, "\n")
|
b.WriteString("-- Default fallback\n")
|
||||||
|
b.WriteString("hl.monitor({ output = \"\", mode = \"preferred\", position = \"auto\", scale = \"auto\" })\n")
|
||||||
|
if err := os.WriteFile(outputsPath, []byte(b.String()), 0o644); err != nil {
|
||||||
|
return newConfig, err
|
||||||
|
}
|
||||||
|
cd.log("Migrated monitor sections to dms/outputs.lua")
|
||||||
|
return newConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
|
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
|
||||||
|
|||||||
@@ -259,130 +259,56 @@ func getGhosttyPath() string {
|
|||||||
func TestMergeHyprlandMonitorSections(t *testing.T) {
|
func TestMergeHyprlandMonitorSections(t *testing.T) {
|
||||||
cd := &ConfigDeployer{}
|
cd := &ConfigDeployer{}
|
||||||
|
|
||||||
tests := []struct {
|
t.Run("no monitors in existing", func(t *testing.T) {
|
||||||
name string
|
tmp := t.TempDir()
|
||||||
newConfig string
|
out, err := cd.mergeHyprlandMonitorSections(`hl.config({})`, `input { kb_layout = us }`, tmp)
|
||||||
existingConfig string
|
require.NoError(t, err)
|
||||||
wantError bool
|
assert.Equal(t, `hl.config({})`, out)
|
||||||
wantContains []string
|
_, e := os.Stat(filepath.Join(tmp, "outputs.lua"))
|
||||||
wantNotContains []string
|
assert.True(t, os.IsNotExist(e))
|
||||||
}{
|
})
|
||||||
{
|
|
||||||
name: "no existing monitors",
|
|
||||||
newConfig: `# ==================
|
|
||||||
# MONITOR CONFIG
|
|
||||||
# ==================
|
|
||||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
|
||||||
|
|
||||||
# ==================
|
t.Run("writes outputs lua from hyprlang monitors", func(t *testing.T) {
|
||||||
# ENVIRONMENT VARS
|
tmp := t.TempDir()
|
||||||
# ==================
|
existing := `monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||||
env = XDG_CURRENT_DESKTOP,niri`,
|
|
||||||
existingConfig: `# Some other config
|
|
||||||
input {
|
|
||||||
kb_layout = us
|
|
||||||
}`,
|
|
||||||
wantError: false,
|
|
||||||
wantContains: []string{"MONITOR CONFIG", "ENVIRONMENT VARS"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "merge single monitor",
|
|
||||||
newConfig: `# ==================
|
|
||||||
# MONITOR CONFIG
|
|
||||||
# ==================
|
|
||||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# ENVIRONMENT VARS
|
|
||||||
# ==================`,
|
|
||||||
existingConfig: `# My config
|
|
||||||
monitor = DP-1, 1920x1080@144, 0x0, 1
|
|
||||||
input {
|
|
||||||
kb_layout = us
|
|
||||||
}`,
|
|
||||||
wantError: false,
|
|
||||||
wantContains: []string{
|
|
||||||
"MONITOR CONFIG",
|
|
||||||
"monitor = DP-1, 1920x1080@144, 0x0, 1",
|
|
||||||
"Monitors from existing configuration",
|
|
||||||
},
|
|
||||||
wantNotContains: []string{
|
|
||||||
"monitor = eDP-2", // Example monitor should be removed
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "merge multiple monitors",
|
|
||||||
newConfig: `# ==================
|
|
||||||
# MONITOR CONFIG
|
|
||||||
# ==================
|
|
||||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# ENVIRONMENT VARS
|
|
||||||
# ==================`,
|
|
||||||
existingConfig: `monitor = DP-1, 1920x1080@144, 0x0, 1
|
|
||||||
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1
|
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1
|
||||||
monitor = eDP-1, 2560x1440@165, auto, 1.25`,
|
monitor = eDP-1, 2560x1440@165, auto, 1.25`
|
||||||
wantError: false,
|
out, err := cd.mergeHyprlandMonitorSections(`return`, existing, tmp)
|
||||||
wantContains: []string{
|
require.NoError(t, err)
|
||||||
"monitor = DP-1",
|
assert.Equal(t, `return`, out)
|
||||||
"# monitor = HDMI-A-1", // Commented monitor preserved
|
b, err := os.ReadFile(filepath.Join(tmp, "outputs.lua"))
|
||||||
"monitor = eDP-1",
|
require.NoError(t, err)
|
||||||
"Monitors from existing configuration",
|
s := string(b)
|
||||||
},
|
assert.Contains(t, s, "hl.monitor")
|
||||||
wantNotContains: []string{
|
assert.Contains(t, s, "DP-1")
|
||||||
"monitor = eDP-2", // Example monitor should be removed
|
assert.Contains(t, s, "HDMI-A-1")
|
||||||
},
|
assert.Contains(t, s, "eDP-1")
|
||||||
},
|
assert.Contains(t, s, "preferred") // fallback rule at end
|
||||||
{
|
})
|
||||||
name: "preserve commented monitors",
|
|
||||||
newConfig: `# ==================
|
|
||||||
# MONITOR CONFIG
|
|
||||||
# ==================
|
|
||||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
|
||||||
|
|
||||||
# ==================`,
|
t.Run("skips when outputs lua already exists", func(t *testing.T) {
|
||||||
existingConfig: `# monitor = DP-1, 1920x1080@144, 0x0, 1
|
tmp := t.TempDir()
|
||||||
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1`,
|
path := filepath.Join(tmp, "outputs.lua")
|
||||||
wantError: false,
|
require.NoError(t, os.WriteFile(path, []byte("-- keep\n"), 0o644))
|
||||||
wantContains: []string{
|
_, err := cd.mergeHyprlandMonitorSections(`x`, `monitor = DP-1, 1920x1080@144, 0x0, 1`, tmp)
|
||||||
"# monitor = DP-1",
|
require.NoError(t, err)
|
||||||
"# monitor = HDMI-A-1",
|
b, err := os.ReadFile(path)
|
||||||
"Monitors from existing configuration",
|
require.NoError(t, err)
|
||||||
},
|
assert.Equal(t, "-- keep\n", string(b))
|
||||||
},
|
})
|
||||||
{
|
}
|
||||||
name: "no monitor config section",
|
|
||||||
newConfig: `# Some config without monitor section
|
|
||||||
input {
|
|
||||||
kb_layout = us
|
|
||||||
}`,
|
|
||||||
existingConfig: `monitor = DP-1, 1920x1080@144, 0x0, 1`,
|
|
||||||
wantError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
func TestHyprlangMonitorLineToLuaPreservesOptions(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
got, err := hyprlangMonitorLineToLua(`monitor = DP-1, 1920x1080@144, 0x0, 1, transform, 1, vrr, 2, bitdepth, 10, cm, hdr, sdrbrightness, 1.2, sdrsaturation, 0.98`)
|
||||||
tmpDir := t.TempDir()
|
require.NoError(t, err)
|
||||||
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig, tmpDir)
|
|
||||||
|
|
||||||
if tt.wantError {
|
assert.Contains(t, got, `output = "DP-1"`)
|
||||||
assert.Error(t, err)
|
assert.Contains(t, got, `transform = 1`)
|
||||||
return
|
assert.Contains(t, got, `vrr = 2`)
|
||||||
}
|
assert.Contains(t, got, `bitdepth = 10`)
|
||||||
|
assert.Contains(t, got, `cm = "hdr"`)
|
||||||
require.NoError(t, err)
|
assert.Contains(t, got, `sdrbrightness = 1.2`)
|
||||||
|
assert.Contains(t, got, `sdrsaturation = 0.98`)
|
||||||
for _, want := range tt.wantContains {
|
|
||||||
assert.Contains(t, result, want, "merged config should contain: %s", want)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, notWant := range tt.wantNotContains {
|
|
||||||
assert.NotContains(t, result, notWant, "merged config should NOT contain: %s", notWant)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHyprlandConfigDeployment(t *testing.T) {
|
func TestHyprlandConfigDeployment(t *testing.T) {
|
||||||
@@ -398,6 +324,10 @@ func TestHyprlandConfigDeployment(t *testing.T) {
|
|||||||
cd := NewConfigDeployer(logChan)
|
cd := NewConfigDeployer(logChan)
|
||||||
|
|
||||||
t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
|
t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
|
||||||
|
td, err := os.MkdirTemp("", "dankinstall-hyprland-empty")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
os.Setenv("HOME", td)
|
||||||
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true)
|
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -408,12 +338,16 @@ func TestHyprlandConfigDeployment(t *testing.T) {
|
|||||||
|
|
||||||
content, err := os.ReadFile(result.Path)
|
content, err := os.ReadFile(result.Path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
assert.Contains(t, string(content), `require("dms.binds")`)
|
||||||
assert.Contains(t, string(content), "source = ./dms/binds.conf")
|
assert.Contains(t, string(content), "DMS_STARTUP_BEGIN")
|
||||||
assert.Contains(t, string(content), "exec-once = ")
|
assert.Contains(t, string(content), "hl.config(")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("deploy hyprland config with existing monitors", func(t *testing.T) {
|
t.Run("deploy hyprland config with existing monitors", func(t *testing.T) {
|
||||||
|
td, err := os.MkdirTemp("", "dankinstall-hyprland-merge")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
os.Setenv("HOME", td)
|
||||||
existingContent := `# My existing Hyprland config
|
existingContent := `# My existing Hyprland config
|
||||||
monitor = DP-1, 1920x1080@144, 0x0, 1
|
monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||||
monitor = HDMI-A-1, 3840x2160@60, 1920x0, 1.5
|
monitor = HDMI-A-1, 3840x2160@60, 1920x0, 1.5
|
||||||
@@ -422,11 +356,17 @@ general {
|
|||||||
gaps_in = 10
|
gaps_in = 10
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
hyprPath := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
hyprPath := filepath.Join(td, ".config", "hypr", "hyprland.conf")
|
||||||
err := os.MkdirAll(filepath.Dir(hyprPath), 0o755)
|
err = os.MkdirAll(filepath.Dir(hyprPath), 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = os.WriteFile(hyprPath, []byte(existingContent), 0o644)
|
err = os.WriteFile(hyprPath, []byte(existingContent), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
dmsDir := filepath.Join(td, ".config", "hypr", "dms")
|
||||||
|
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds.conf"), []byte("bind = SUPER, T, exec, foot\n"), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "cursor.conf"), []byte("env = XCURSOR_SIZE,24\n"), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(filepath.Dir(hyprPath), "hyprland.conf.backup.old"), []byte("old backup\n"), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds.conf.backup.old"), []byte("old dms backup\n"), 0o644))
|
||||||
|
|
||||||
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -440,13 +380,76 @@ general {
|
|||||||
backupContent, err := os.ReadFile(result.BackupPath)
|
backupContent, err := os.ReadFile(result.BackupPath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, existingContent, string(backupContent))
|
assert.Equal(t, existingContent, string(backupContent))
|
||||||
|
assert.Contains(t, result.BackupPath, hyprlandBackupDirName)
|
||||||
|
assert.NoFileExists(t, hyprPath)
|
||||||
|
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "binds.conf"))
|
||||||
|
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "cursor.conf"))
|
||||||
|
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "hyprland.conf.backup.old"))
|
||||||
|
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "binds.conf.backup.old"))
|
||||||
|
assert.NoFileExists(t, filepath.Join(dmsDir, "binds.conf"))
|
||||||
|
assert.NoFileExists(t, filepath.Join(dmsDir, "cursor.conf"))
|
||||||
|
assert.NoFileExists(t, filepath.Join(filepath.Dir(hyprPath), "hyprland.conf.backup.old"))
|
||||||
|
assert.NoFileExists(t, filepath.Join(dmsDir, "binds.conf.backup.old"))
|
||||||
|
|
||||||
newContent, err := os.ReadFile(result.Path)
|
newContent, err := os.ReadFile(result.Path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
|
assert.Contains(t, string(newContent), `require("dms.binds")`)
|
||||||
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
|
|
||||||
assert.Contains(t, string(newContent), "source = ./dms/binds.conf")
|
outputsPath := filepath.Join(td, ".config", "hypr", "dms", "outputs.lua")
|
||||||
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
outBytes, err := os.ReadFile(outputsPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
outs := string(outBytes)
|
||||||
|
assert.Contains(t, outs, `hl.monitor`)
|
||||||
|
assert.Contains(t, outs, "DP-1")
|
||||||
|
assert.Contains(t, outs, "HDMI-A-1")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deploy hyprland config removes root legacy symlink when lua exists", func(t *testing.T) {
|
||||||
|
td, err := os.MkdirTemp("", "dankinstall-hyprland-lua-conf-symlink")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
os.Setenv("HOME", td)
|
||||||
|
|
||||||
|
configDir := filepath.Join(td, ".config", "hypr")
|
||||||
|
require.NoError(t, os.MkdirAll(configDir, 0o755))
|
||||||
|
luaPath := filepath.Join(configDir, "hyprland.lua")
|
||||||
|
confPath := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
require.NoError(t, os.WriteFile(luaPath, []byte(`require("dms.binds")`+"\n"), 0o644))
|
||||||
|
require.NoError(t, os.Symlink(filepath.Join(configDir, "missing-legacy.conf"), confPath))
|
||||||
|
|
||||||
|
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, luaPath, result.Path)
|
||||||
|
_, err = os.Lstat(confPath)
|
||||||
|
assert.True(t, os.IsNotExist(err), "root hyprland.conf symlink should be moved out of the live config directory")
|
||||||
|
_, err = os.Lstat(filepath.Join(filepath.Dir(result.BackupPath), "hyprland.conf"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deploy hyprland config refreshes managed binds but preserves user binds", func(t *testing.T) {
|
||||||
|
td, err := os.MkdirTemp("", "dankinstall-hyprland-refresh-binds")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
os.Setenv("HOME", td)
|
||||||
|
|
||||||
|
dmsDir := filepath.Join(td, ".config", "hypr", "dms")
|
||||||
|
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds.lua"), []byte("-- stale managed binds\n"), 0o644))
|
||||||
|
userBinds := "-- custom user binds\n"
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte(userBinds), 0o644))
|
||||||
|
|
||||||
|
_, err = cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
managed, err := os.ReadFile(filepath.Join(dmsDir, "binds.lua"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(managed), `hl.bind("SUPER + F", hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" }))`)
|
||||||
|
assert.Contains(t, string(managed), `hl.bind("SUPER + minus", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive -10% 0]]), { repeating = true })`)
|
||||||
|
|
||||||
|
user, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, userBinds, string(user))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,10 +462,10 @@ func TestNiriConfigStructure(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHyprlandConfigStructure(t *testing.T) {
|
func TestHyprlandConfigStructure(t *testing.T) {
|
||||||
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
assert.Contains(t, HyprlandLuaConfig, `require("dms.binds")`)
|
||||||
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
assert.Contains(t, HyprlandLuaConfig, "DMS_STARTUP_BEGIN")
|
||||||
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
assert.Contains(t, HyprlandLuaConfig, "hl.config(")
|
||||||
assert.Contains(t, HyprlandConfig, "source = ./dms/binds.conf")
|
assert.Contains(t, HyprlandLuaConfig, "input =")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGhosttyConfigStructure(t *testing.T) {
|
func TestGhosttyConfigStructure(t *testing.T) {
|
||||||
@@ -789,4 +792,37 @@ func TestShouldReplaceConfigDeployIfMissing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.True(t, foundGhostty, "expected Ghostty config to be deployed when replaceConfigs is true")
|
assert.True(t, foundGhostty, "expected Ghostty config to be deployed when replaceConfigs is true")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("hyprland legacy config exists skips when replace false", func(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "dankinstall-hyprland-legacy-skip-test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
|
||||||
|
hyprConf := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Dir(hyprConf), 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(hyprConf, []byte("monitor = , preferred, auto, 1\n"), 0o644))
|
||||||
|
|
||||||
|
logChan := make(chan string, 100)
|
||||||
|
cd := NewConfigDeployer(logChan)
|
||||||
|
results, err := cd.deployConfigurationsInternal(
|
||||||
|
context.Background(),
|
||||||
|
deps.WindowManagerHyprland,
|
||||||
|
deps.TerminalGhostty,
|
||||||
|
nil,
|
||||||
|
allFalse,
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, r := range results {
|
||||||
|
if r.ConfigType == "Hyprland" && r.Deployed {
|
||||||
|
t.Fatalf("expected Hyprland deployment to be skipped when legacy config exists and replace=false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
-- Optional per-user keybind overrides (managed by DMS). Loaded after default binds.
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
# === Application Launchers ===
|
|
||||||
bind = SUPER, T, exec, {{TERMINAL_COMMAND}}
|
|
||||||
bind = SUPER, space, exec, dms ipc call spotlight toggle
|
|
||||||
bind = SUPER, V, exec, dms ipc call clipboard toggle
|
|
||||||
bind = SUPER, M, exec, dms ipc call processlist focusOrToggle
|
|
||||||
bind = SUPER, comma, exec, dms ipc call settings focusOrToggle
|
|
||||||
bind = SUPER, N, exec, dms ipc call notifications toggle
|
|
||||||
bind = SUPER SHIFT, N, exec, dms ipc call notepad toggle
|
|
||||||
bind = SUPER, Y, exec, dms ipc call dankdash wallpaper
|
|
||||||
bind = SUPER, TAB, exec, dms ipc call hypr toggleOverview
|
|
||||||
bind = SUPER, X, exec, dms ipc call powermenu toggle
|
|
||||||
|
|
||||||
# === Cheat sheet
|
|
||||||
bind = SUPER SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
|
|
||||||
|
|
||||||
# === Security ===
|
|
||||||
bind = SUPER ALT, L, exec, dms ipc call lock lock
|
|
||||||
bind = SUPER SHIFT, E, exit
|
|
||||||
bind = CTRL ALT, Delete, exec, dms ipc call processlist focusOrToggle
|
|
||||||
|
|
||||||
# === Audio Controls ===
|
|
||||||
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
|
|
||||||
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
|
|
||||||
bindl = , XF86AudioMute, exec, dms ipc call audio mute
|
|
||||||
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
|
|
||||||
bindl = , XF86AudioPause, exec, dms ipc call mpris playPause
|
|
||||||
bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause
|
|
||||||
bindl = , XF86AudioPrev, exec, dms ipc call mpris previous
|
|
||||||
bindl = , XF86AudioNext, exec, dms ipc call mpris next
|
|
||||||
bindel = CTRL, XF86AudioRaiseVolume, exec, dms ipc call mpris increment 3
|
|
||||||
bindel = CTRL, XF86AudioLowerVolume, exec, dms ipc call mpris decrement 3
|
|
||||||
|
|
||||||
# === Brightness Controls ===
|
|
||||||
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
|
||||||
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
|
|
||||||
|
|
||||||
# === Window Management ===
|
|
||||||
bind = SUPER, Q, killactive
|
|
||||||
bind = SUPER, F, fullscreen, 1
|
|
||||||
bind = SUPER SHIFT, F, fullscreen, 0
|
|
||||||
bind = SUPER SHIFT, T, togglefloating
|
|
||||||
bind = SUPER, W, togglegroup
|
|
||||||
bind = SUPER SHIFT, W, exec, dms ipc call window-rules toggle
|
|
||||||
|
|
||||||
# === Focus Navigation ===
|
|
||||||
bind = SUPER, left, movefocus, l
|
|
||||||
bind = SUPER, down, movefocus, d
|
|
||||||
bind = SUPER, up, movefocus, u
|
|
||||||
bind = SUPER, right, movefocus, r
|
|
||||||
bind = SUPER, H, movefocus, l
|
|
||||||
bind = SUPER, J, movefocus, d
|
|
||||||
bind = SUPER, K, movefocus, u
|
|
||||||
bind = SUPER, L, movefocus, r
|
|
||||||
|
|
||||||
# === Window Movement ===
|
|
||||||
bind = SUPER SHIFT, left, movewindow, l
|
|
||||||
bind = SUPER SHIFT, down, movewindow, d
|
|
||||||
bind = SUPER SHIFT, up, movewindow, u
|
|
||||||
bind = SUPER SHIFT, right, movewindow, r
|
|
||||||
bind = SUPER SHIFT, H, movewindow, l
|
|
||||||
bind = SUPER SHIFT, J, movewindow, d
|
|
||||||
bind = SUPER SHIFT, K, movewindow, u
|
|
||||||
bind = SUPER SHIFT, L, movewindow, r
|
|
||||||
|
|
||||||
# === Column Navigation ===
|
|
||||||
bind = SUPER, Home, focuswindow, first
|
|
||||||
bind = SUPER, End, focuswindow, last
|
|
||||||
|
|
||||||
# === Monitor Navigation ===
|
|
||||||
bind = SUPER CTRL, left, focusmonitor, l
|
|
||||||
bind = SUPER CTRL, right, focusmonitor, r
|
|
||||||
bind = SUPER CTRL, H, focusmonitor, l
|
|
||||||
bind = SUPER CTRL, J, focusmonitor, d
|
|
||||||
bind = SUPER CTRL, K, focusmonitor, u
|
|
||||||
bind = SUPER CTRL, L, focusmonitor, r
|
|
||||||
|
|
||||||
# === Move to Monitor ===
|
|
||||||
bind = SUPER SHIFT CTRL, left, movewindow, mon:l
|
|
||||||
bind = SUPER SHIFT CTRL, down, movewindow, mon:d
|
|
||||||
bind = SUPER SHIFT CTRL, up, movewindow, mon:u
|
|
||||||
bind = SUPER SHIFT CTRL, right, movewindow, mon:r
|
|
||||||
bind = SUPER SHIFT CTRL, H, movewindow, mon:l
|
|
||||||
bind = SUPER SHIFT CTRL, J, movewindow, mon:d
|
|
||||||
bind = SUPER SHIFT CTRL, K, movewindow, mon:u
|
|
||||||
bind = SUPER SHIFT CTRL, L, movewindow, mon:r
|
|
||||||
|
|
||||||
# === Workspace Navigation ===
|
|
||||||
bind = SUPER, Page_Down, workspace, e+1
|
|
||||||
bind = SUPER, Page_Up, workspace, e-1
|
|
||||||
bind = SUPER, U, workspace, e+1
|
|
||||||
bind = SUPER, I, workspace, e-1
|
|
||||||
bind = SUPER CTRL, down, movetoworkspace, e+1
|
|
||||||
bind = SUPER CTRL, up, movetoworkspace, e-1
|
|
||||||
bind = SUPER CTRL, U, movetoworkspace, e+1
|
|
||||||
bind = SUPER CTRL, I, movetoworkspace, e-1
|
|
||||||
|
|
||||||
# === Workspace Management ===
|
|
||||||
bind = CTRL SHIFT, R, exec, dms ipc call workspace-rename open
|
|
||||||
|
|
||||||
# === Move Workspaces ===
|
|
||||||
bind = SUPER SHIFT, Page_Down, movetoworkspace, e+1
|
|
||||||
bind = SUPER SHIFT, Page_Up, movetoworkspace, e-1
|
|
||||||
bind = SUPER SHIFT, U, movetoworkspace, e+1
|
|
||||||
bind = SUPER SHIFT, I, movetoworkspace, e-1
|
|
||||||
|
|
||||||
# === Mouse Wheel Navigation ===
|
|
||||||
bind = SUPER, mouse_down, workspace, e+1
|
|
||||||
bind = SUPER, mouse_up, workspace, e-1
|
|
||||||
bind = SUPER CTRL, mouse_down, movetoworkspace, e+1
|
|
||||||
bind = SUPER CTRL, mouse_up, movetoworkspace, e-1
|
|
||||||
|
|
||||||
# === Numbered Workspaces ===
|
|
||||||
bind = SUPER, 1, workspace, 1
|
|
||||||
bind = SUPER, 2, workspace, 2
|
|
||||||
bind = SUPER, 3, workspace, 3
|
|
||||||
bind = SUPER, 4, workspace, 4
|
|
||||||
bind = SUPER, 5, workspace, 5
|
|
||||||
bind = SUPER, 6, workspace, 6
|
|
||||||
bind = SUPER, 7, workspace, 7
|
|
||||||
bind = SUPER, 8, workspace, 8
|
|
||||||
bind = SUPER, 9, workspace, 9
|
|
||||||
|
|
||||||
# === Move to Numbered Workspaces ===
|
|
||||||
bind = SUPER SHIFT, 1, movetoworkspace, 1
|
|
||||||
bind = SUPER SHIFT, 2, movetoworkspace, 2
|
|
||||||
bind = SUPER SHIFT, 3, movetoworkspace, 3
|
|
||||||
bind = SUPER SHIFT, 4, movetoworkspace, 4
|
|
||||||
bind = SUPER SHIFT, 5, movetoworkspace, 5
|
|
||||||
bind = SUPER SHIFT, 6, movetoworkspace, 6
|
|
||||||
bind = SUPER SHIFT, 7, movetoworkspace, 7
|
|
||||||
bind = SUPER SHIFT, 8, movetoworkspace, 8
|
|
||||||
bind = SUPER SHIFT, 9, movetoworkspace, 9
|
|
||||||
|
|
||||||
# === Column Management ===
|
|
||||||
bind = SUPER, bracketleft, layoutmsg, preselect l
|
|
||||||
bind = SUPER, bracketright, layoutmsg, preselect r
|
|
||||||
|
|
||||||
# === Sizing & Layout ===
|
|
||||||
bind = SUPER, R, layoutmsg, togglesplit
|
|
||||||
bind = SUPER CTRL, F, resizeactive, exact 100% 100%
|
|
||||||
|
|
||||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
|
||||||
bindmd = SUPER, mouse:272, Move window, movewindow
|
|
||||||
bindmd = SUPER, mouse:273, Resize window, resizewindow
|
|
||||||
|
|
||||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
|
||||||
bindd = SUPER, code:20, Expand window left, resizeactive, -100 0
|
|
||||||
bindd = SUPER, code:21, Shrink window left, resizeactive, 100 0
|
|
||||||
|
|
||||||
# === Manual Sizing ===
|
|
||||||
binde = SUPER, minus, resizeactive, -10% 0
|
|
||||||
binde = SUPER, equal, resizeactive, 10% 0
|
|
||||||
binde = SUPER SHIFT, minus, resizeactive, 0 -10%
|
|
||||||
binde = SUPER SHIFT, equal, resizeactive, 0 10%
|
|
||||||
|
|
||||||
# === Screenshots ===
|
|
||||||
bind = , Print, exec, dms screenshot
|
|
||||||
bind = CTRL, Print, exec, dms screenshot full
|
|
||||||
bind = ALT, Print, exec, dms screenshot window
|
|
||||||
|
|
||||||
# === Display Profiles ===
|
|
||||||
bind = SUPER, P, exec, dms ipc outputs cycleProfile
|
|
||||||
|
|
||||||
# === System Controls ===
|
|
||||||
bind = SUPER SHIFT, P, dpms, toggle
|
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
-- DMS default keybinds (Hyprland 0.55+ Lua)
|
||||||
|
|
||||||
|
-- === Application Launchers ===
|
||||||
|
hl.bind("SUPER + T", hl.dsp.exec_cmd("{{TERMINAL_COMMAND}}"))
|
||||||
|
hl.bind("SUPER + space", hl.dsp.exec_cmd("dms ipc call spotlight toggle"))
|
||||||
|
hl.bind("SUPER + V", hl.dsp.exec_cmd("dms ipc call clipboard toggle"))
|
||||||
|
hl.bind("SUPER + M", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle"))
|
||||||
|
hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle"))
|
||||||
|
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle"))
|
||||||
|
hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
||||||
|
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dankdash wallpaper"))
|
||||||
|
hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
||||||
|
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
|
||||||
|
|
||||||
|
-- === Cheat sheet
|
||||||
|
hl.bind("SUPER + SHIFT + Slash", hl.dsp.exec_cmd("dms ipc call keybinds toggle hyprland"))
|
||||||
|
|
||||||
|
-- === Security ===
|
||||||
|
hl.bind("SUPER + ALT + L", hl.dsp.exec_cmd("dms ipc call lock lock"))
|
||||||
|
hl.bind("SUPER + SHIFT + E", hl.dsp.exit())
|
||||||
|
hl.bind("CTRL + ALT + Delete", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle"))
|
||||||
|
|
||||||
|
-- === Audio Controls ===
|
||||||
|
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("dms ipc call audio increment 3"), { locked = true, repeating = true })
|
||||||
|
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("dms ipc call audio decrement 3"), { locked = true, repeating = true })
|
||||||
|
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("dms ipc call audio mute"), { locked = true })
|
||||||
|
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("dms ipc call audio micmute"), { locked = true })
|
||||||
|
hl.bind("XF86AudioPause", hl.dsp.exec_cmd("dms ipc call mpris playPause"), { locked = true })
|
||||||
|
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("dms ipc call mpris playPause"), { locked = true })
|
||||||
|
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("dms ipc call mpris previous"), { locked = true })
|
||||||
|
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("dms ipc call mpris next"), { locked = true })
|
||||||
|
hl.bind("CTRL + XF86AudioRaiseVolume", hl.dsp.exec_cmd("dms ipc call mpris increment 3"), { locked = true, repeating = true })
|
||||||
|
hl.bind("CTRL + XF86AudioLowerVolume", hl.dsp.exec_cmd("dms ipc call mpris decrement 3"), { locked = true, repeating = true })
|
||||||
|
|
||||||
|
-- === Brightness Controls ===
|
||||||
|
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd([[dms ipc call brightness increment 5 ""]]), { locked = true, repeating = true })
|
||||||
|
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd([[dms ipc call brightness decrement 5 ""]]), { locked = true, repeating = true })
|
||||||
|
|
||||||
|
-- === Window Management ===
|
||||||
|
hl.bind("SUPER + Q", hl.dsp.window.kill())
|
||||||
|
hl.bind("SUPER + F", hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" }))
|
||||||
|
hl.bind("SUPER + SHIFT + F", hl.dsp.window.fullscreen({ mode = "fullscreen", action = "toggle" }))
|
||||||
|
hl.bind("SUPER + SHIFT + T", hl.dsp.window.float({ action = "toggle" }))
|
||||||
|
hl.bind("SUPER + W", hl.dsp.group.toggle())
|
||||||
|
hl.bind("SUPER + SHIFT + W", hl.dsp.exec_cmd("dms ipc call window-rules toggle"))
|
||||||
|
|
||||||
|
-- === Focus Navigation ===
|
||||||
|
hl.bind("SUPER + left", hl.dsp.focus({ direction = "l" }))
|
||||||
|
hl.bind("SUPER + down", hl.dsp.focus({ direction = "d" }))
|
||||||
|
hl.bind("SUPER + up", hl.dsp.focus({ direction = "u" }))
|
||||||
|
hl.bind("SUPER + right", hl.dsp.focus({ direction = "r" }))
|
||||||
|
hl.bind("SUPER + H", hl.dsp.focus({ direction = "l" }))
|
||||||
|
hl.bind("SUPER + J", hl.dsp.focus({ direction = "d" }))
|
||||||
|
hl.bind("SUPER + K", hl.dsp.focus({ direction = "u" }))
|
||||||
|
hl.bind("SUPER + L", hl.dsp.focus({ direction = "r" }))
|
||||||
|
|
||||||
|
-- === Window Movement ===
|
||||||
|
hl.bind("SUPER + SHIFT + left", hl.dsp.window.move({ direction = "l" }))
|
||||||
|
hl.bind("SUPER + SHIFT + down", hl.dsp.window.move({ direction = "d" }))
|
||||||
|
hl.bind("SUPER + SHIFT + up", hl.dsp.window.move({ direction = "u" }))
|
||||||
|
hl.bind("SUPER + SHIFT + right", hl.dsp.window.move({ direction = "r" }))
|
||||||
|
hl.bind("SUPER + SHIFT + H", hl.dsp.window.move({ direction = "l" }))
|
||||||
|
hl.bind("SUPER + SHIFT + J", hl.dsp.window.move({ direction = "d" }))
|
||||||
|
hl.bind("SUPER + SHIFT + K", hl.dsp.window.move({ direction = "u" }))
|
||||||
|
hl.bind("SUPER + SHIFT + L", hl.dsp.window.move({ direction = "r" }))
|
||||||
|
|
||||||
|
-- === Column Navigation ===
|
||||||
|
hl.bind("SUPER + Home", hl.dsp.focus({ window = "first" }))
|
||||||
|
hl.bind("SUPER + End", hl.dsp.focus({ window = "last" }))
|
||||||
|
|
||||||
|
-- === Monitor Navigation ===
|
||||||
|
hl.bind("SUPER + CTRL + left", hl.dsp.focus({ monitor = "l" }))
|
||||||
|
hl.bind("SUPER + CTRL + right", hl.dsp.focus({ monitor = "r" }))
|
||||||
|
hl.bind("SUPER + CTRL + H", hl.dsp.focus({ monitor = "l" }))
|
||||||
|
hl.bind("SUPER + CTRL + J", hl.dsp.focus({ monitor = "d" }))
|
||||||
|
hl.bind("SUPER + CTRL + K", hl.dsp.focus({ monitor = "u" }))
|
||||||
|
hl.bind("SUPER + CTRL + L", hl.dsp.focus({ monitor = "r" }))
|
||||||
|
|
||||||
|
-- === Move to Monitor ===
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + left", hl.dsp.window.move({ monitor = "l" }))
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + down", hl.dsp.window.move({ monitor = "d" }))
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + up", hl.dsp.window.move({ monitor = "u" }))
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + right", hl.dsp.window.move({ monitor = "r" }))
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + H", hl.dsp.window.move({ monitor = "l" }))
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + J", hl.dsp.window.move({ monitor = "d" }))
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + K", hl.dsp.window.move({ monitor = "u" }))
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + L", hl.dsp.window.move({ monitor = "r" }))
|
||||||
|
|
||||||
|
-- === Workspace Navigation ===
|
||||||
|
hl.bind("SUPER + Page_Down", hl.dsp.focus({ workspace = "e+1" }))
|
||||||
|
hl.bind("SUPER + Page_Up", hl.dsp.focus({ workspace = "e-1" }))
|
||||||
|
hl.bind("SUPER + U", hl.dsp.focus({ workspace = "e+1" }))
|
||||||
|
hl.bind("SUPER + I", hl.dsp.focus({ workspace = "e-1" }))
|
||||||
|
hl.bind("SUPER + CTRL + down", hl.dsp.window.move({ workspace = "e+1" }))
|
||||||
|
hl.bind("SUPER + CTRL + up", hl.dsp.window.move({ workspace = "e-1" }))
|
||||||
|
hl.bind("SUPER + CTRL + U", hl.dsp.window.move({ workspace = "e+1" }))
|
||||||
|
hl.bind("SUPER + CTRL + I", hl.dsp.window.move({ workspace = "e-1" }))
|
||||||
|
|
||||||
|
-- === Workspace Management ===
|
||||||
|
hl.bind("CTRL + SHIFT + R", hl.dsp.exec_cmd("dms ipc call workspace-rename open"))
|
||||||
|
|
||||||
|
-- === Move Workspaces ===
|
||||||
|
hl.bind("SUPER + SHIFT + Page_Down", hl.dsp.window.move({ workspace = "e+1" }))
|
||||||
|
hl.bind("SUPER + SHIFT + Page_Up", hl.dsp.window.move({ workspace = "e-1" }))
|
||||||
|
hl.bind("SUPER + SHIFT + U", hl.dsp.window.move({ workspace = "e+1" }))
|
||||||
|
hl.bind("SUPER + SHIFT + I", hl.dsp.window.move({ workspace = "e-1" }))
|
||||||
|
|
||||||
|
-- === Mouse Wheel Navigation ===
|
||||||
|
hl.bind("SUPER + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
|
||||||
|
hl.bind("SUPER + mouse_up", hl.dsp.focus({ workspace = "e-1" }))
|
||||||
|
hl.bind("SUPER + CTRL + mouse_down", hl.dsp.window.move({ workspace = "e+1" }))
|
||||||
|
hl.bind("SUPER + CTRL + mouse_up", hl.dsp.window.move({ workspace = "e-1" }))
|
||||||
|
|
||||||
|
-- === Numbered Workspaces ===
|
||||||
|
hl.bind("SUPER + 1", hl.dsp.focus({ workspace = "1" }))
|
||||||
|
hl.bind("SUPER + 2", hl.dsp.focus({ workspace = "2" }))
|
||||||
|
hl.bind("SUPER + 3", hl.dsp.focus({ workspace = "3" }))
|
||||||
|
hl.bind("SUPER + 4", hl.dsp.focus({ workspace = "4" }))
|
||||||
|
hl.bind("SUPER + 5", hl.dsp.focus({ workspace = "5" }))
|
||||||
|
hl.bind("SUPER + 6", hl.dsp.focus({ workspace = "6" }))
|
||||||
|
hl.bind("SUPER + 7", hl.dsp.focus({ workspace = "7" }))
|
||||||
|
hl.bind("SUPER + 8", hl.dsp.focus({ workspace = "8" }))
|
||||||
|
hl.bind("SUPER + 9", hl.dsp.focus({ workspace = "9" }))
|
||||||
|
|
||||||
|
-- === Move to Numbered Workspaces ===
|
||||||
|
hl.bind("SUPER + SHIFT + 1", hl.dsp.window.move({ workspace = "1" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 2", hl.dsp.window.move({ workspace = "2" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 3", hl.dsp.window.move({ workspace = "3" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 4", hl.dsp.window.move({ workspace = "4" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 5", hl.dsp.window.move({ workspace = "5" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 6", hl.dsp.window.move({ workspace = "6" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 7", hl.dsp.window.move({ workspace = "7" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 8", hl.dsp.window.move({ workspace = "8" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 9", hl.dsp.window.move({ workspace = "9" }))
|
||||||
|
|
||||||
|
-- === Column Management ===
|
||||||
|
hl.bind("SUPER + bracketleft", hl.dsp.layout("preselect l"))
|
||||||
|
hl.bind("SUPER + bracketright", hl.dsp.layout("preselect r"))
|
||||||
|
|
||||||
|
-- === Sizing & Layout ===
|
||||||
|
hl.bind("SUPER + R", hl.dsp.layout("togglesplit"))
|
||||||
|
hl.bind("SUPER + CTRL + F", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive exact 100% 100%]]))
|
||||||
|
|
||||||
|
-- === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||||
|
hl.bind("SUPER + mouse:272", hl.dsp.window.drag(), { mouse = true, description = "Move window" })
|
||||||
|
hl.bind("SUPER + mouse:273", hl.dsp.window.resize(), { mouse = true, description = "Resize window" })
|
||||||
|
|
||||||
|
hl.bind("SUPER + code:20", hl.dsp.window.resize({ x = -100, y = 0, relative = true }), { description = "Expand window left" })
|
||||||
|
hl.bind("SUPER + code:21", hl.dsp.window.resize({ x = 100, y = 0, relative = true }), { description = "Shrink window left" })
|
||||||
|
|
||||||
|
-- === Manual Sizing ===
|
||||||
|
hl.bind("SUPER + minus", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive -10% 0]]), { repeating = true })
|
||||||
|
hl.bind("SUPER + equal", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive 10% 0]]), { repeating = true })
|
||||||
|
hl.bind("SUPER + SHIFT + minus", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive 0 -10%]]), { repeating = true })
|
||||||
|
hl.bind("SUPER + SHIFT + equal", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive 0 10%]]), { repeating = true })
|
||||||
|
|
||||||
|
-- === Screenshots ===
|
||||||
|
hl.bind("Print", hl.dsp.exec_cmd("dms screenshot"))
|
||||||
|
hl.bind("CTRL + Print", hl.dsp.exec_cmd("dms screenshot full"))
|
||||||
|
hl.bind("ALT + Print", hl.dsp.exec_cmd("dms screenshot window"))
|
||||||
|
|
||||||
|
-- === Display Profiles ===
|
||||||
|
hl.bind("SUPER + P", hl.dsp.exec_cmd("dms ipc outputs cycleProfile"))
|
||||||
|
|
||||||
|
-- === System Controls ===
|
||||||
|
hl.bind("SUPER + SHIFT + P", hl.dsp.dpms({ action = "toggle" }))
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# ! Auto-generated file. Do not edit directly.
|
|
||||||
# Remove source = ./dms/colors.conf from your config to override.
|
|
||||||
|
|
||||||
$primary = rgb(d0bcff)
|
|
||||||
$outline = rgb(948f99)
|
|
||||||
$error = rgb(f2b8b5)
|
|
||||||
|
|
||||||
general {
|
|
||||||
col.active_border = $primary
|
|
||||||
col.inactive_border = $outline
|
|
||||||
}
|
|
||||||
|
|
||||||
group {
|
|
||||||
col.border_active = $primary
|
|
||||||
col.border_inactive = $outline
|
|
||||||
col.border_locked_active = $error
|
|
||||||
col.border_locked_inactive = $outline
|
|
||||||
|
|
||||||
groupbar {
|
|
||||||
col.active = $primary
|
|
||||||
col.inactive = $outline
|
|
||||||
col.locked_active = $error
|
|
||||||
col.locked_inactive = $outline
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- ! Auto-generated file. Do not edit directly.
|
||||||
|
-- Regenerate via DMS theme tools or remove require("dms.colors") from hyprland.lua to override.
|
||||||
|
|
||||||
|
hl.config({
|
||||||
|
general = {
|
||||||
|
col = {
|
||||||
|
active_border = "rgb(d0bcff)",
|
||||||
|
inactive_border = "rgb(948f99)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
group = {
|
||||||
|
col = {
|
||||||
|
border_active = "rgb(d0bcff)",
|
||||||
|
border_inactive = "rgb(948f99)",
|
||||||
|
border_locked_active = "rgb(f2b8b5)",
|
||||||
|
border_locked_inactive = "rgb(948f99)",
|
||||||
|
},
|
||||||
|
groupbar = {
|
||||||
|
col = {
|
||||||
|
active = "rgb(d0bcff)",
|
||||||
|
inactive = "rgb(948f99)",
|
||||||
|
locked_active = "rgb(f2b8b5)",
|
||||||
|
locked_inactive = "rgb(948f99)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-- Cursor theme overrides. Deploy writes ~/.config/hypr/dms/cursor.lua
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Auto-generated by DMS - do not edit manually
|
|
||||||
|
|
||||||
general {
|
|
||||||
gaps_in = 4
|
|
||||||
gaps_out = 4
|
|
||||||
border_size = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
decoration {
|
|
||||||
rounding = 12
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- Auto-generated by DMS — do not edit manually
|
||||||
|
|
||||||
|
hl.config({
|
||||||
|
general = {
|
||||||
|
gaps_in = 4,
|
||||||
|
gaps_out = 4,
|
||||||
|
border_size = 2,
|
||||||
|
},
|
||||||
|
decoration = {
|
||||||
|
rounding = 12,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Per-output monitor rules — embedded sibling of the legacy outputs.conf fragment. Deploy writes ~/.config/hypr/dms/outputs.lua
|
||||||
|
|
||||||
|
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = "auto" })
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-- Window rules. Deploy writes ~/.config/hypr/dms/windowrules.lua
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
# Hyprland Configuration
|
|
||||||
# https://wiki.hypr.land/Configuring/
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# MONITOR CONFIG
|
|
||||||
# ==================
|
|
||||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
|
||||||
monitor = , preferred,auto,auto
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# STARTUP APPS
|
|
||||||
# ==================
|
|
||||||
exec-once = dbus-update-activation-environment --systemd --all
|
|
||||||
exec-once = systemctl --user start hyprland-session.target
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# INPUT CONFIG
|
|
||||||
# ==================
|
|
||||||
input {
|
|
||||||
kb_layout = us
|
|
||||||
numlock_by_default = true
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# GENERAL LAYOUT
|
|
||||||
# ==================
|
|
||||||
general {
|
|
||||||
gaps_in = 5
|
|
||||||
gaps_out = 5
|
|
||||||
border_size = 2
|
|
||||||
|
|
||||||
layout = dwindle
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# DECORATION
|
|
||||||
# ==================
|
|
||||||
decoration {
|
|
||||||
rounding = 12
|
|
||||||
|
|
||||||
active_opacity = 1.0
|
|
||||||
inactive_opacity = 1.0
|
|
||||||
|
|
||||||
shadow {
|
|
||||||
enabled = true
|
|
||||||
range = 30
|
|
||||||
render_power = 5
|
|
||||||
offset = 0 5
|
|
||||||
color = rgba(00000070)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# ANIMATIONS
|
|
||||||
# ==================
|
|
||||||
animations {
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
animation = windowsIn, 1, 3, default
|
|
||||||
animation = windowsOut, 1, 3, default
|
|
||||||
animation = workspaces, 1, 5, default
|
|
||||||
animation = windowsMove, 1, 4, default
|
|
||||||
animation = fade, 1, 3, default
|
|
||||||
animation = border, 1, 3, default
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# LAYOUTS
|
|
||||||
# ==================
|
|
||||||
dwindle {
|
|
||||||
preserve_split = true
|
|
||||||
}
|
|
||||||
|
|
||||||
master {
|
|
||||||
mfact = 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# MISC
|
|
||||||
# ==================
|
|
||||||
misc {
|
|
||||||
disable_hyprland_logo = true
|
|
||||||
disable_splash_rendering = true
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# WINDOW RULES
|
|
||||||
# ==================
|
|
||||||
windowrule = tile on, match:class ^(org\.wezfurlong\.wezterm)$
|
|
||||||
|
|
||||||
windowrule = rounding 12, match:class ^(org\.gnome\.)
|
|
||||||
|
|
||||||
windowrule = tile on, match:class ^(gnome-control-center)$
|
|
||||||
windowrule = tile on, match:class ^(pavucontrol)$
|
|
||||||
windowrule = tile on, match:class ^(nm-connection-editor)$
|
|
||||||
|
|
||||||
windowrule = float on, match:class ^(org\.gnome\.Calculator)$
|
|
||||||
windowrule = float on, match:class ^(gnome-calculator)$
|
|
||||||
windowrule = float on, match:class ^(galculator)$
|
|
||||||
windowrule = float on, match:class ^(blueman-manager)$
|
|
||||||
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
|
|
||||||
windowrule = float on, match:class ^(xdg-desktop-portal)$
|
|
||||||
|
|
||||||
windowrule = no_initial_focus on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
|
||||||
windowrule = pin on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
|
||||||
|
|
||||||
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
|
|
||||||
windowrule = float on, match:class ^(zoom)$
|
|
||||||
|
|
||||||
layerrule = no_anim on, match:namespace ^(quickshell)$
|
|
||||||
layerrule = no_anim on, match:namespace ^dms:.*
|
|
||||||
|
|
||||||
source = ./dms/colors.conf
|
|
||||||
source = ./dms/outputs.conf
|
|
||||||
source = ./dms/layout.conf
|
|
||||||
source = ./dms/cursor.conf
|
|
||||||
source = ./dms/binds.conf
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
-- Hyprland configuration (Lua) — https://wiki.hypr.land/Configuring/Start/
|
||||||
|
|
||||||
|
hl.config({ autogenerated = false })
|
||||||
|
|
||||||
|
-- DMS_STARTUP_BEGIN
|
||||||
|
hl.on("hyprland.start", function()
|
||||||
|
hl.exec_cmd("dbus-update-activation-environment --systemd --all")
|
||||||
|
hl.exec_cmd("systemctl --user start hyprland-session.target")
|
||||||
|
end)
|
||||||
|
-- DMS_STARTUP_END
|
||||||
|
|
||||||
|
hl.config({
|
||||||
|
input = {
|
||||||
|
kb_layout = "us",
|
||||||
|
numlock_by_default = true,
|
||||||
|
},
|
||||||
|
general = {
|
||||||
|
gaps_in = 5,
|
||||||
|
gaps_out = 5,
|
||||||
|
border_size = 2,
|
||||||
|
layout = "dwindle",
|
||||||
|
},
|
||||||
|
decoration = {
|
||||||
|
rounding = 12,
|
||||||
|
active_opacity = 1.0,
|
||||||
|
inactive_opacity = 1.0,
|
||||||
|
shadow = {
|
||||||
|
enabled = true,
|
||||||
|
range = 30,
|
||||||
|
render_power = 5,
|
||||||
|
offset = "0 5",
|
||||||
|
color = "rgba(00000070)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
misc = {
|
||||||
|
disable_hyprland_logo = true,
|
||||||
|
disable_splash_rendering = true,
|
||||||
|
},
|
||||||
|
dwindle = {
|
||||||
|
preserve_split = true,
|
||||||
|
},
|
||||||
|
master = {
|
||||||
|
mfact = 0.5,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
hl.animation({ leaf = "windowsIn", enabled = true, speed = 3, bezier = "default" })
|
||||||
|
hl.animation({ leaf = "windowsOut", enabled = true, speed = 3, bezier = "default" })
|
||||||
|
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "default" })
|
||||||
|
hl.animation({ leaf = "windowsMove", enabled = true, speed = 4, bezier = "default" })
|
||||||
|
hl.animation({ leaf = "fade", enabled = true, speed = 3, bezier = "default" })
|
||||||
|
hl.animation({ leaf = "border", enabled = true, speed = 3, bezier = "default" })
|
||||||
|
|
||||||
|
hl.window_rule({ match = { class = "^(org\\.wezfurlong\\.wezterm)$" }, tile = true })
|
||||||
|
hl.window_rule({ match = { class = "^(org\\.gnome\\.)" }, rounding = 12 })
|
||||||
|
hl.window_rule({ match = { class = "^(gnome-control-center)$" }, tile = true })
|
||||||
|
hl.window_rule({ match = { class = "^(pavucontrol)$" }, tile = true })
|
||||||
|
hl.window_rule({ match = { class = "^(nm-connection-editor)$" }, tile = true })
|
||||||
|
hl.window_rule({ match = { class = "^(org\\.gnome\\.Calculator)$" }, float = true })
|
||||||
|
hl.window_rule({ match = { class = "^(gnome-calculator)$" }, float = true })
|
||||||
|
hl.window_rule({ match = { class = "^(galculator)$" }, float = true })
|
||||||
|
hl.window_rule({ match = { class = "^(blueman-manager)$" }, float = true })
|
||||||
|
hl.window_rule({ match = { class = "^(org\\.gnome\\.Nautilus)$" }, float = true })
|
||||||
|
hl.window_rule({ match = { class = "^(xdg-desktop-portal)$" }, float = true })
|
||||||
|
hl.window_rule({
|
||||||
|
match = { class = "^(steam)$", title = "^(notificationtoasts)" },
|
||||||
|
no_initial_focus = true,
|
||||||
|
pin = true,
|
||||||
|
})
|
||||||
|
hl.window_rule({
|
||||||
|
match = { class = "^(firefox)$", title = "^(Picture-in-Picture)$" },
|
||||||
|
float = true,
|
||||||
|
})
|
||||||
|
hl.window_rule({ match = { class = "^(zoom)$" }, float = true })
|
||||||
|
hl.layer_rule({ match = { namespace = "^(quickshell)$" }, no_anim = true })
|
||||||
|
hl.layer_rule({ match = { namespace = "^dms:.*" }, no_anim = true })
|
||||||
|
|
||||||
|
require("dms.colors")
|
||||||
|
require("dms.outputs")
|
||||||
|
require("dms.layout")
|
||||||
|
require("dms.cursor")
|
||||||
|
require("dms.binds")
|
||||||
|
require("dms.binds-user")
|
||||||
|
require("dms.windowrules")
|
||||||
@@ -2,14 +2,26 @@ package config
|
|||||||
|
|
||||||
import _ "embed"
|
import _ "embed"
|
||||||
|
|
||||||
//go:embed embedded/hyprland.conf
|
//go:embed embedded/hyprland.lua
|
||||||
var HyprlandConfig string
|
var HyprlandLuaConfig string
|
||||||
|
|
||||||
//go:embed embedded/hypr-colors.conf
|
//go:embed embedded/hypr-colors.lua
|
||||||
var HyprColorsConfig string
|
var DMSColorsLuaConfig string
|
||||||
|
|
||||||
//go:embed embedded/hypr-layout.conf
|
//go:embed embedded/hypr-layout.lua
|
||||||
var HyprLayoutConfig string
|
var DMSLayoutLuaConfig string
|
||||||
|
|
||||||
//go:embed embedded/hypr-binds.conf
|
//go:embed embedded/hypr-binds.lua
|
||||||
var HyprBindsConfig string
|
var DMSBindsLuaConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/hypr-outputs.lua
|
||||||
|
var DMSOutputsLuaConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/hypr-cursor.lua
|
||||||
|
var DMSCursorLuaConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/hypr-windowrules.lua
|
||||||
|
var DMSWindowRulesLuaConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/hypr-binds-user.lua
|
||||||
|
var DMSBindsUserLuaConfig string
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hyprlandStartupBegin = "-- DMS_STARTUP_BEGIN"
|
||||||
|
hyprlandStartupEnd = "-- DMS_STARTUP_END"
|
||||||
|
)
|
||||||
|
|
||||||
|
func extractHyprlangMonitorLines(hyprlang string) []string {
|
||||||
|
re := regexp.MustCompile(`(?m)^\s*#?\s*monitor\s*=.*$`)
|
||||||
|
return re.FindAllString(hyprlang, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hyprlangMonitorLineToLua(line string) (string, error) {
|
||||||
|
re := regexp.MustCompile(`(?i)^\s*#?\s*monitor\s*=\s*(.*)\s*$`)
|
||||||
|
m := re.FindStringSubmatch(line)
|
||||||
|
if m == nil {
|
||||||
|
return "", fmt.Errorf("not a monitor line")
|
||||||
|
}
|
||||||
|
rest := strings.TrimSpace(m[1])
|
||||||
|
parts := strings.Split(rest, ",")
|
||||||
|
for i := range parts {
|
||||||
|
parts[i] = strings.TrimSpace(parts[i])
|
||||||
|
}
|
||||||
|
if len(parts) < 4 {
|
||||||
|
if len(parts) == 2 && strings.EqualFold(parts[1], "disable") {
|
||||||
|
return fmt.Sprintf(`hl.monitor({ output = %s, disabled = true })`, strconv.Quote(parts[0])), nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("expected at least 4 comma-separated fields")
|
||||||
|
}
|
||||||
|
out := parts[0]
|
||||||
|
mode := parts[1]
|
||||||
|
pos := parts[2]
|
||||||
|
scaleStr := parts[3]
|
||||||
|
|
||||||
|
scaleField := formatMonitorScaleLua(scaleStr)
|
||||||
|
fields := []string{
|
||||||
|
fmt.Sprintf("output = %s", strconv.Quote(out)),
|
||||||
|
fmt.Sprintf("mode = %s", strconv.Quote(mode)),
|
||||||
|
fmt.Sprintf("position = %s", strconv.Quote(pos)),
|
||||||
|
scaleField,
|
||||||
|
}
|
||||||
|
for i := 4; i < len(parts); i += 2 {
|
||||||
|
key := strings.ToLower(strings.TrimSpace(parts[i]))
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i+1 >= len(parts) {
|
||||||
|
fields = append(fields, fmt.Sprintf("%s = true", hyprlangMonitorOptionToLuaKey(key)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val := strings.TrimSpace(parts[i+1])
|
||||||
|
if converted, ok := formatMonitorOptionLua(key, val); ok {
|
||||||
|
fields = append(fields, converted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`hl.monitor({ %s })`, strings.Join(fields, ", ")), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMonitorScaleLua(scaleStr string) string {
|
||||||
|
if scaleStr == "auto" {
|
||||||
|
return `scale = "auto"`
|
||||||
|
}
|
||||||
|
if f, err := strconv.ParseFloat(scaleStr, 64); err == nil {
|
||||||
|
return fmt.Sprintf(`scale = %g`, f)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`scale = %s`, strconv.Quote(scaleStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func hyprlangMonitorOptionToLuaKey(key string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(key)) {
|
||||||
|
case "10bit":
|
||||||
|
return "bitdepth"
|
||||||
|
default:
|
||||||
|
return strings.ReplaceAll(strings.ToLower(strings.TrimSpace(key)), "-", "_")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMonitorOptionLua(key, val string) (string, bool) {
|
||||||
|
luaKey := hyprlangMonitorOptionToLuaKey(key)
|
||||||
|
switch luaKey {
|
||||||
|
case "transform", "vrr", "bitdepth", "supports_wide_color", "supports_hdr", "sdr_max_luminance", "max_luminance", "max_avg_luminance":
|
||||||
|
if _, err := strconv.Atoi(val); err == nil {
|
||||||
|
return fmt.Sprintf("%s = %s", luaKey, val), true
|
||||||
|
}
|
||||||
|
case "sdrbrightness", "sdrsaturation", "sdr_min_luminance", "min_luminance":
|
||||||
|
if _, err := strconv.ParseFloat(val, 64); err == nil {
|
||||||
|
return fmt.Sprintf("%s = %s", luaKey, val), true
|
||||||
|
}
|
||||||
|
case "cm", "sdr_eotf", "icc", "mirror":
|
||||||
|
return fmt.Sprintf("%s = %s", luaKey, strconv.Quote(val)), true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformHyprlandLuaForNonSystemd(config, terminalCommand string) string {
|
||||||
|
start := strings.Index(config, hyprlandStartupBegin)
|
||||||
|
end := strings.Index(config, hyprlandStartupEnd)
|
||||||
|
if start == -1 || end == -1 || end <= start {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
endClose := end + len(hyprlandStartupEnd)
|
||||||
|
replacement := hyprlandStartupBegin + "\n" +
|
||||||
|
`hl.env("QT_QPA_PLATFORM", "wayland;xcb")` + "\n" +
|
||||||
|
`hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto")` + "\n" +
|
||||||
|
`hl.env("QT_QPA_PLATFORMTHEME", "gtk3")` + "\n" +
|
||||||
|
`hl.env("QT_QPA_PLATFORMTHEME_QT6", "gtk3")` + "\n" +
|
||||||
|
fmt.Sprintf(`hl.env("TERMINAL", %s)`, strconv.Quote(terminalCommand)) + "\n\n" +
|
||||||
|
`hl.on("hyprland.start", function()` + "\n" +
|
||||||
|
` hl.exec_cmd("dms run")` + "\n" +
|
||||||
|
`end)` + "\n" +
|
||||||
|
hyprlandStartupEnd
|
||||||
|
return config[:start] + replacement + config[endClose:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func readExistingHyprlandConfig(configDir string) (data string, sourcePath string, err error) {
|
||||||
|
luaPath := filepath.Join(configDir, "hyprland.lua")
|
||||||
|
if b, e := os.ReadFile(luaPath); e == nil {
|
||||||
|
return string(b), luaPath, nil
|
||||||
|
} else if !os.IsNotExist(e) {
|
||||||
|
return "", "", e
|
||||||
|
}
|
||||||
|
confPath := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
if b, e := os.ReadFile(confPath); e == nil {
|
||||||
|
return string(b), confPath, nil
|
||||||
|
} else if !os.IsNotExist(e) {
|
||||||
|
return "", "", e
|
||||||
|
}
|
||||||
|
return "", "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupStrayHyprlandConfFile moves a stray ~/.config/hypr/hyprland.conf
|
||||||
|
// into .dms-backups/<timestamp>/ when running under Hyprland. Hyprland 0.55
|
||||||
|
// auto-generates hyprland.conf when launched without -c, so this is invoked
|
||||||
|
// from dms run startup to keep the active config tree single-file.
|
||||||
|
func CleanupStrayHyprlandConfFile(logFn func(format string, v ...any)) {
|
||||||
|
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
if home == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configDir := filepath.Join(home, ".config", "hypr")
|
||||||
|
confPath := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
if _, err := os.Stat(confPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts := time.Now().Format("2006-01-02_15-04-05")
|
||||||
|
dst := filepath.Join(configDir, hyprlandBackupDirName, ts, "hyprland.conf")
|
||||||
|
if err := moveHyprlandConfigFile(confPath, dst); err != nil {
|
||||||
|
if logFn != nil {
|
||||||
|
logFn("Could not move stray hyprland.conf: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if logFn != nil {
|
||||||
|
logFn("Moved stray hyprland.conf to %s", dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
@@ -48,7 +49,7 @@ func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
|||||||
h.parsed = true
|
h.parsed = true
|
||||||
|
|
||||||
categorizedBinds := make(map[string][]keybinds.Keybind)
|
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||||
h.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs)
|
h.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs, result.DefaultDMSKeys)
|
||||||
|
|
||||||
sheet := &keybinds.CheatSheet{
|
sheet := &keybinds.CheatSheet{
|
||||||
Title: "Hyprland Keybinds",
|
Title: "Hyprland Keybinds",
|
||||||
@@ -88,7 +89,7 @@ func (h *HyprlandProvider) HasDMSBindsIncluded() bool {
|
|||||||
return h.dmsBindsIncluded
|
return h.dmsBindsIncluded
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*HyprlandKeyBinding) {
|
func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*HyprlandKeyBinding, defaultKeys map[string]bool) {
|
||||||
currentSubcat := subcategory
|
currentSubcat := subcategory
|
||||||
if section.Name != "" {
|
if section.Name != "" {
|
||||||
currentSubcat = section.Name
|
currentSubcat = section.Name
|
||||||
@@ -96,12 +97,12 @@ func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory
|
|||||||
|
|
||||||
for _, kb := range section.Keybinds {
|
for _, kb := range section.Keybinds {
|
||||||
category := h.categorizeByDispatcher(kb.Dispatcher)
|
category := h.categorizeByDispatcher(kb.Dispatcher)
|
||||||
bind := h.convertKeybind(&kb, currentSubcat, conflicts)
|
bind := h.convertKeybind(&kb, currentSubcat, conflicts, defaultKeys)
|
||||||
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, child := range section.Children {
|
for _, child := range section.Children {
|
||||||
h.convertSection(&child, currentSubcat, categorizedBinds, conflicts)
|
h.convertSection(&child, currentSubcat, categorizedBinds, conflicts, defaultKeys)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +134,7 @@ func (h *HyprlandProvider) categorizeByDispatcher(dispatcher string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string, conflicts map[string]*HyprlandKeyBinding) keybinds.Keybind {
|
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string, conflicts map[string]*HyprlandKeyBinding, defaultKeys map[string]bool) keybinds.Keybind {
|
||||||
keyStr := h.formatKey(kb)
|
keyStr := h.formatKey(kb)
|
||||||
rawAction := h.formatRawAction(kb.Dispatcher, kb.Params)
|
rawAction := h.formatRawAction(kb.Dispatcher, kb.Params)
|
||||||
desc := kb.Comment
|
desc := kb.Comment
|
||||||
@@ -143,8 +144,15 @@ func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory st
|
|||||||
}
|
}
|
||||||
|
|
||||||
source := "config"
|
source := "config"
|
||||||
if strings.Contains(kb.Source, "dms/binds.conf") {
|
if isDMSBindsUserOverridePath(kb.Source) {
|
||||||
source = "dms"
|
source = "dms"
|
||||||
|
} else if isDMSBindsPrimarySourcePath(kb.Source) {
|
||||||
|
source = "dms-default"
|
||||||
|
}
|
||||||
|
|
||||||
|
hasDefault := false
|
||||||
|
if source == "dms" && defaultKeys != nil {
|
||||||
|
hasDefault = defaultKeys[strings.ToLower(keyStr)]
|
||||||
}
|
}
|
||||||
|
|
||||||
bind := keybinds.Keybind{
|
bind := keybinds.Keybind{
|
||||||
@@ -154,9 +162,10 @@ func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory st
|
|||||||
Subcategory: subcategory,
|
Subcategory: subcategory,
|
||||||
Source: source,
|
Source: source,
|
||||||
Flags: kb.Flags,
|
Flags: kb.Flags,
|
||||||
|
HasDefault: hasDefault,
|
||||||
}
|
}
|
||||||
|
|
||||||
if source == "dms" && conflicts != nil {
|
if (source == "dms" || source == "dms-default") && conflicts != nil {
|
||||||
normalizedKey := strings.ToLower(keyStr)
|
normalizedKey := strings.ToLower(keyStr)
|
||||||
if conflictKb, ok := conflicts[normalizedKey]; ok {
|
if conflictKb, ok := conflicts[normalizedKey]; ok {
|
||||||
bind.Conflict = &keybinds.Keybind{
|
bind.Conflict = &keybinds.Keybind{
|
||||||
@@ -188,9 +197,9 @@ func (h *HyprlandProvider) formatKey(kb *HyprlandKeyBinding) string {
|
|||||||
func (h *HyprlandProvider) GetOverridePath() string {
|
func (h *HyprlandProvider) GetOverridePath() string {
|
||||||
expanded, err := utils.ExpandPath(h.configPath)
|
expanded, err := utils.ExpandPath(h.configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return filepath.Join(h.configPath, "dms", "binds.conf")
|
return filepath.Join(h.configPath, "dms", "binds-user.lua")
|
||||||
}
|
}
|
||||||
return filepath.Join(expanded, "dms", "binds.conf")
|
return filepath.Join(expanded, "dms", "binds-user.lua")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) validateAction(action string) error {
|
func (h *HyprlandProvider) validateAction(action string) error {
|
||||||
@@ -250,7 +259,16 @@ func (h *HyprlandProvider) RemoveBind(key string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
normalizedKey := strings.ToLower(key)
|
||||||
|
existingBinds[normalizedKey] = &hyprlandOverrideBind{Key: key, Unbind: true}
|
||||||
|
return h.writeOverrideBinds(existingBinds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HyprlandProvider) ResetBind(key string) error {
|
||||||
|
existingBinds, err := h.loadOverrideBinds()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
normalizedKey := strings.ToLower(key)
|
normalizedKey := strings.ToLower(key)
|
||||||
delete(existingBinds, normalizedKey)
|
delete(existingBinds, normalizedKey)
|
||||||
return h.writeOverrideBinds(existingBinds)
|
return h.writeOverrideBinds(existingBinds)
|
||||||
@@ -262,116 +280,12 @@ type hyprlandOverrideBind struct {
|
|||||||
Description string
|
Description string
|
||||||
Flags string // Bind flags: l=locked, r=release, e=repeat, n=non-consuming, m=mouse, t=transparent, i=ignore-mods, s=separate, d=description, o=long-press
|
Flags string // Bind flags: l=locked, r=release, e=repeat, n=non-consuming, m=mouse, t=transparent, i=ignore-mods, s=separate, d=description, o=long-press
|
||||||
Options map[string]any
|
Options map[string]any
|
||||||
|
// Unbind: negative override (hl.unbind only, no rebind).
|
||||||
|
Unbind bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind, error) {
|
func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind, error) {
|
||||||
overridePath := h.GetOverridePath()
|
return readLuaOrHyprlangOverride(h.GetOverridePath())
|
||||||
binds := make(map[string]*hyprlandOverrideBind)
|
|
||||||
|
|
||||||
data, err := os.ReadFile(overridePath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return binds, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := strings.Split(string(data), "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(line, "bind") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(line, "=", 2)
|
|
||||||
if len(parts) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract flags from bind type
|
|
||||||
bindType := strings.TrimSpace(parts[0])
|
|
||||||
flags := extractBindFlags(bindType)
|
|
||||||
hasDescFlag := strings.Contains(flags, "d")
|
|
||||||
|
|
||||||
content := strings.TrimSpace(parts[1])
|
|
||||||
commentParts := strings.SplitN(content, "#", 2)
|
|
||||||
bindContent := strings.TrimSpace(commentParts[0])
|
|
||||||
|
|
||||||
var comment string
|
|
||||||
if len(commentParts) > 1 {
|
|
||||||
comment = strings.TrimSpace(commentParts[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
// For bindd, format is: mods, key, description, dispatcher, params
|
|
||||||
var minFields, descIndex, dispatcherIndex int
|
|
||||||
if hasDescFlag {
|
|
||||||
minFields = 4
|
|
||||||
descIndex = 2
|
|
||||||
dispatcherIndex = 3
|
|
||||||
} else {
|
|
||||||
minFields = 3
|
|
||||||
dispatcherIndex = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
fields := strings.SplitN(bindContent, ",", minFields+2)
|
|
||||||
if len(fields) < minFields {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mods := strings.TrimSpace(fields[0])
|
|
||||||
keyName := strings.TrimSpace(fields[1])
|
|
||||||
|
|
||||||
var dispatcher, params string
|
|
||||||
if hasDescFlag {
|
|
||||||
if comment == "" {
|
|
||||||
comment = strings.TrimSpace(fields[descIndex])
|
|
||||||
}
|
|
||||||
dispatcher = strings.TrimSpace(fields[dispatcherIndex])
|
|
||||||
if len(fields) > dispatcherIndex+1 {
|
|
||||||
paramParts := fields[dispatcherIndex+1:]
|
|
||||||
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dispatcher = strings.TrimSpace(fields[dispatcherIndex])
|
|
||||||
if len(fields) > dispatcherIndex+1 {
|
|
||||||
paramParts := fields[dispatcherIndex+1:]
|
|
||||||
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keyStr := h.buildKeyString(mods, keyName)
|
|
||||||
normalizedKey := strings.ToLower(keyStr)
|
|
||||||
action := dispatcher
|
|
||||||
if params != "" {
|
|
||||||
action = dispatcher + " " + params
|
|
||||||
}
|
|
||||||
|
|
||||||
binds[normalizedKey] = &hyprlandOverrideBind{
|
|
||||||
Key: keyStr,
|
|
||||||
Action: action,
|
|
||||||
Description: comment,
|
|
||||||
Flags: flags,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return binds, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HyprlandProvider) buildKeyString(mods, key string) string {
|
|
||||||
if mods == "" {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
modList := strings.FieldsFunc(mods, func(r rune) bool {
|
|
||||||
return r == '+' || r == ' '
|
|
||||||
})
|
|
||||||
|
|
||||||
parts := append(modList, key)
|
|
||||||
return strings.Join(parts, "+")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) getBindSortPriority(action string) int {
|
func (h *HyprlandProvider) getBindSortPriority(action string) int {
|
||||||
@@ -420,78 +334,203 @@ func (h *HyprlandProvider) generateBindsContent(binds map[string]*hyprlandOverri
|
|||||||
})
|
})
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
sb.WriteString("-- DMS user keybind overrides (edit via Control Center or dms; do not remove this header)\n\n")
|
||||||
for _, bind := range bindList {
|
for _, bind := range bindList {
|
||||||
h.writeBindLine(&sb, bind)
|
writeLuaBindLine(&sb, bind)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) writeBindLine(sb *strings.Builder, bind *hyprlandOverrideBind) {
|
func formatLuaBindKey(internalKey string) string {
|
||||||
mods, key := h.parseKeyString(bind.Key)
|
internalKey = strings.TrimSpace(internalKey)
|
||||||
dispatcher, params := h.parseAction(bind.Action)
|
parts := strings.Split(internalKey, "+")
|
||||||
|
for i := range parts {
|
||||||
// Write bind type with flags (e.g., "bind", "binde", "bindel")
|
parts[i] = normalizeLuaBindKeyPart(strings.TrimSpace(parts[i]))
|
||||||
sb.WriteString("bind")
|
|
||||||
if bind.Flags != "" {
|
|
||||||
sb.WriteString(bind.Flags)
|
|
||||||
}
|
}
|
||||||
sb.WriteString(" = ")
|
return strings.Join(parts, " + ")
|
||||||
sb.WriteString(mods)
|
|
||||||
sb.WriteString(", ")
|
|
||||||
sb.WriteString(key)
|
|
||||||
sb.WriteString(", ")
|
|
||||||
|
|
||||||
// For bindd (description flag), include description before dispatcher
|
|
||||||
if strings.Contains(bind.Flags, "d") && bind.Description != "" {
|
|
||||||
sb.WriteString(bind.Description)
|
|
||||||
sb.WriteString(", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.WriteString(dispatcher)
|
|
||||||
|
|
||||||
if params != "" {
|
|
||||||
sb.WriteString(", ")
|
|
||||||
sb.WriteString(params)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only add comment if not using bindd (which has inline description)
|
|
||||||
if bind.Description != "" && !strings.Contains(bind.Flags, "d") {
|
|
||||||
sb.WriteString(" # ")
|
|
||||||
sb.WriteString(bind.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.WriteString("\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) parseKeyString(keyStr string) (mods, key string) {
|
func normalizeLuaBindKeyPart(part string) string {
|
||||||
parts := strings.Split(keyStr, "+")
|
switch strings.ToLower(part) {
|
||||||
switch len(parts) {
|
case "super", "mod4", "mainmod":
|
||||||
case 0:
|
return "SUPER"
|
||||||
return "", keyStr
|
case "ctrl", "control":
|
||||||
case 1:
|
return "CTRL"
|
||||||
return "", parts[0]
|
case "shift":
|
||||||
|
return "SHIFT"
|
||||||
|
case "alt", "mod1":
|
||||||
|
return "ALT"
|
||||||
|
}
|
||||||
|
if len(part) == 1 {
|
||||||
|
return strings.ToUpper(part)
|
||||||
|
}
|
||||||
|
return part
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaActionStringFromHyprlangAction(action string) string {
|
||||||
|
action = strings.TrimSpace(action)
|
||||||
|
if strings.HasPrefix(action, "spawn ") {
|
||||||
|
return fmt.Sprintf(`hl.dsp.exec_cmd(%s)`, strconv.Quote(strings.TrimSpace(strings.TrimPrefix(action, "spawn "))))
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(action, "exec ") {
|
||||||
|
return fmt.Sprintf(`hl.dsp.exec_cmd(%s)`, strconv.Quote(strings.TrimPrefix(action, "exec ")))
|
||||||
|
}
|
||||||
|
switch action {
|
||||||
|
case "killactive":
|
||||||
|
return `hl.dsp.window.kill()`
|
||||||
|
case "togglefloating":
|
||||||
|
return `hl.dsp.window.float({ action = "toggle" })`
|
||||||
|
case "exit":
|
||||||
|
return `hl.dsp.exit()`
|
||||||
default:
|
default:
|
||||||
return strings.Join(parts[:len(parts)-1], " "), parts[len(parts)-1]
|
return fmt.Sprintf(`hl.dsp.exec_cmd(%s)`, strconv.Quote("hyprctl dispatch "+action))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) parseAction(action string) (dispatcher, params string) {
|
func luaExprToInternalAction(expr string) string {
|
||||||
parts := strings.SplitN(action, " ", 2)
|
d, p := luaExprToDispatcherParams(expr)
|
||||||
switch len(parts) {
|
if d == "exec" && p != "" && !strings.HasPrefix(p, "hyprctl dispatch lua:") {
|
||||||
case 0:
|
return "exec " + p
|
||||||
return action, ""
|
|
||||||
case 1:
|
|
||||||
dispatcher = parts[0]
|
|
||||||
default:
|
|
||||||
dispatcher = parts[0]
|
|
||||||
params = parts[1]
|
|
||||||
}
|
}
|
||||||
|
if p != "" {
|
||||||
// Convert internal spawn format to Hyprland's exec
|
return d + " " + p
|
||||||
if dispatcher == "spawn" {
|
|
||||||
dispatcher = "exec"
|
|
||||||
}
|
}
|
||||||
|
return d
|
||||||
return dispatcher, params
|
}
|
||||||
|
|
||||||
|
func luaBindOptions(bind *hyprlandOverrideBind) []string {
|
||||||
|
var opts []string
|
||||||
|
if strings.Contains(bind.Flags, "l") {
|
||||||
|
opts = append(opts, "locked = true")
|
||||||
|
}
|
||||||
|
if strings.Contains(bind.Flags, "e") {
|
||||||
|
opts = append(opts, "repeating = true")
|
||||||
|
}
|
||||||
|
if bind.Description != "" && strings.Contains(bind.Flags, "d") {
|
||||||
|
opts = append(opts, fmt.Sprintf("description = %s", strconv.Quote(bind.Description)))
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeLuaBindLine(sb *strings.Builder, bind *hyprlandOverrideBind) {
|
||||||
|
key := formatLuaBindKey(bind.Key)
|
||||||
|
if bind.Unbind {
|
||||||
|
fmt.Fprintf(sb, `hl.unbind("%s")`, key)
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expr := luaActionStringFromHyprlangAction(bind.Action)
|
||||||
|
opts := luaBindOptions(bind)
|
||||||
|
fmt.Fprintf(sb, `hl.unbind("%s")`, key)
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
if len(opts) > 0 {
|
||||||
|
fmt.Fprintf(sb, `hl.bind("%s", %s, { %s })`, key, expr, strings.Join(opts, ", "))
|
||||||
|
} else {
|
||||||
|
if bind.Description != "" {
|
||||||
|
fmt.Fprintf(sb, `hl.bind("%s", %s) -- %s`, key, expr, bind.Description)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(sb, `hl.bind("%s", %s)`, key, expr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLuaBindOverrideLine(line string) (*hyprlandOverrideBind, bool) {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "--") {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
kbc, actionExpr, optSuffix, ok := parseLuaBindInvocation(line)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
internalKey := luaKeyComboToInternalKey(kbc)
|
||||||
|
|
||||||
|
action := luaExprToInternalAction(actionExpr)
|
||||||
|
flags := luaBindOptFlags(optSuffix)
|
||||||
|
description := luaBindOptDescription(optSuffix)
|
||||||
|
return &hyprlandOverrideBind{
|
||||||
|
Key: internalKey,
|
||||||
|
Action: action,
|
||||||
|
Description: description,
|
||||||
|
Flags: flags,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLuaUnbindLine(line string) (string, bool) {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if !strings.HasPrefix(line, "hl.unbind") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
rest := strings.TrimSpace(line[len("hl.unbind"):])
|
||||||
|
if !strings.HasPrefix(rest, "(") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
rest = rest[1:]
|
||||||
|
combo, _, ok := parseLuaStringLiteral(rest, 0)
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return luaKeyComboToInternalKey(combo), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaKeyComboToInternalKey(combo string) string {
|
||||||
|
parts := strings.Fields(strings.ReplaceAll(strings.ReplaceAll(combo, "+", " "), " ", " "))
|
||||||
|
return strings.Join(parts, "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readLuaOrHyprlangOverride(path string) (map[string]*hyprlandOverrideBind, error) {
|
||||||
|
binds := make(map[string]*hyprlandOverrideBind)
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return binds, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
parser := NewHyprlandParser("")
|
||||||
|
pendingUnbinds := make(map[string]string)
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "--") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if key, ok := parseLuaUnbindLine(line); ok {
|
||||||
|
pendingUnbinds[strings.ToLower(key)] = key
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if kb, ok := parseLuaBindOverrideLine(line); ok {
|
||||||
|
normalizedKey := strings.ToLower(kb.Key)
|
||||||
|
binds[normalizedKey] = kb
|
||||||
|
delete(pendingUnbinds, normalizedKey)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(line, "bind") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kb := parser.parseBindLine(line)
|
||||||
|
if kb == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keyStr := parser.formatBindKey(kb)
|
||||||
|
action := kb.Dispatcher
|
||||||
|
if kb.Params != "" {
|
||||||
|
action = kb.Dispatcher + " " + kb.Params
|
||||||
|
}
|
||||||
|
flags := kb.Flags
|
||||||
|
normalizedKey := strings.ToLower(keyStr)
|
||||||
|
binds[normalizedKey] = &hyprlandOverrideBind{
|
||||||
|
Key: keyStr,
|
||||||
|
Action: action,
|
||||||
|
Description: kb.Comment,
|
||||||
|
Flags: flags,
|
||||||
|
}
|
||||||
|
delete(pendingUnbinds, normalizedKey)
|
||||||
|
}
|
||||||
|
for normKey, origKey := range pendingUnbinds {
|
||||||
|
binds[normKey] = &hyprlandOverrideBind{Key: origKey, Unbind: true}
|
||||||
|
}
|
||||||
|
return binds, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/luaconfig"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,6 +52,8 @@ type HyprlandParser struct {
|
|||||||
bindOrder []string
|
bindOrder []string
|
||||||
processedFiles map[string]bool
|
processedFiles map[string]bool
|
||||||
dmsProcessed bool
|
dmsProcessed bool
|
||||||
|
removedKeys map[string]bool // bare hl.unbind targets (negative overrides)
|
||||||
|
defaultDMSKeys map[string]bool // keys present in dms/binds.{lua,conf}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHyprlandParser(configDir string) *HyprlandParser {
|
func NewHyprlandParser(configDir string) *HyprlandParser {
|
||||||
@@ -64,6 +68,8 @@ func NewHyprlandParser(configDir string) *HyprlandParser {
|
|||||||
bindMap: make(map[string]*HyprlandKeyBinding),
|
bindMap: make(map[string]*HyprlandKeyBinding),
|
||||||
bindOrder: []string{},
|
bindOrder: []string{},
|
||||||
processedFiles: make(map[string]bool),
|
processedFiles: make(map[string]bool),
|
||||||
|
removedKeys: make(map[string]bool),
|
||||||
|
defaultDMSKeys: make(map[string]bool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,6 +298,7 @@ type HyprlandParseResult struct {
|
|||||||
DMSBindsIncluded bool
|
DMSBindsIncluded bool
|
||||||
DMSStatus *HyprlandDMSStatus
|
DMSStatus *HyprlandDMSStatus
|
||||||
ConflictingConfigs map[string]*HyprlandKeyBinding
|
ConflictingConfigs map[string]*HyprlandKeyBinding
|
||||||
|
DefaultDMSKeys map[string]bool // keys with a DMS default in binds.{lua,conf}
|
||||||
}
|
}
|
||||||
|
|
||||||
type HyprlandDMSStatus struct {
|
type HyprlandDMSStatus struct {
|
||||||
@@ -317,10 +324,10 @@ func (p *HyprlandParser) buildDMSStatus() *HyprlandDMSStatus {
|
|||||||
switch {
|
switch {
|
||||||
case !p.dmsBindsExists:
|
case !p.dmsBindsExists:
|
||||||
status.Effective = false
|
status.Effective = false
|
||||||
status.StatusMessage = "dms/binds.conf does not exist"
|
status.StatusMessage = "dms/binds.lua (or legacy binds.conf) does not exist"
|
||||||
case !p.dmsBindsIncluded:
|
case !p.dmsBindsIncluded:
|
||||||
status.Effective = false
|
status.Effective = false
|
||||||
status.StatusMessage = "dms/binds.conf is not sourced in config"
|
status.StatusMessage = "dms binds are not loaded from Hyprland config (require / source)"
|
||||||
case p.bindsAfterDMS > 0:
|
case p.bindsAfterDMS > 0:
|
||||||
status.Effective = true
|
status.Effective = true
|
||||||
status.OverriddenBy = p.bindsAfterDMS
|
status.OverriddenBy = p.bindsAfterDMS
|
||||||
@@ -347,8 +354,11 @@ func (p *HyprlandParser) normalizeKey(key string) string {
|
|||||||
func (p *HyprlandParser) addBind(kb *HyprlandKeyBinding) bool {
|
func (p *HyprlandParser) addBind(kb *HyprlandKeyBinding) bool {
|
||||||
key := p.formatBindKey(kb)
|
key := p.formatBindKey(kb)
|
||||||
normalizedKey := p.normalizeKey(key)
|
normalizedKey := p.normalizeKey(key)
|
||||||
isDMSBind := strings.Contains(kb.Source, "dms/binds.conf")
|
isDMSBind := isDMSBindsSourcePath(kb.Source)
|
||||||
|
|
||||||
|
if isDMSBindsPrimarySourcePath(kb.Source) {
|
||||||
|
p.defaultDMSKeys[normalizedKey] = true
|
||||||
|
}
|
||||||
if isDMSBind {
|
if isDMSBind {
|
||||||
p.dmsBindKeys[normalizedKey] = true
|
p.dmsBindKeys[normalizedKey] = true
|
||||||
} else if p.dmsBindKeys[normalizedKey] {
|
} else if p.dmsBindKeys[normalizedKey] {
|
||||||
@@ -373,12 +383,21 @@ func (p *HyprlandParser) ParseWithDMS() (*HyprlandSection, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dmsBindsPath := filepath.Join(expandedDir, "dms", "binds.conf")
|
dmsBindsLua := filepath.Join(expandedDir, "dms", "binds.lua")
|
||||||
if _, err := os.Stat(dmsBindsPath); err == nil {
|
dmsBindsConf := filepath.Join(expandedDir, "dms", "binds.conf")
|
||||||
|
dmsBindsPath := ""
|
||||||
|
if _, err := os.Stat(dmsBindsLua); err == nil {
|
||||||
p.dmsBindsExists = true
|
p.dmsBindsExists = true
|
||||||
|
dmsBindsPath = dmsBindsLua
|
||||||
|
} else if _, err := os.Stat(dmsBindsConf); err == nil {
|
||||||
|
p.dmsBindsExists = true
|
||||||
|
dmsBindsPath = dmsBindsConf
|
||||||
}
|
}
|
||||||
|
|
||||||
mainConfig := filepath.Join(expandedDir, "hyprland.conf")
|
mainConfig, err := hyprlandMainConfigPath(p.configDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
section, err := p.parseFileWithSource(mainConfig, "")
|
section, err := p.parseFileWithSource(mainConfig, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -387,10 +406,65 @@ func (p *HyprlandParser) ParseWithDMS() (*HyprlandSection, error) {
|
|||||||
if p.dmsBindsExists && !p.dmsProcessed {
|
if p.dmsBindsExists && !p.dmsProcessed {
|
||||||
p.parseDMSBindsDirectly(dmsBindsPath, section)
|
p.parseDMSBindsDirectly(dmsBindsPath, section)
|
||||||
}
|
}
|
||||||
|
p.removeShadowedDMSBinds(section)
|
||||||
|
p.removeUnboundDMSBinds(section)
|
||||||
|
|
||||||
return section, nil
|
return section, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) removeUnboundDMSBinds(section *HyprlandSection) {
|
||||||
|
if len(p.removedKeys) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filtered := section.Keybinds[:0]
|
||||||
|
for i := range section.Keybinds {
|
||||||
|
kb := section.Keybinds[i]
|
||||||
|
if isDMSBindsSourcePath(kb.Source) && p.removedKeys[p.normalizeKey(p.formatBindKey(&kb))] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, kb)
|
||||||
|
}
|
||||||
|
section.Keybinds = filtered
|
||||||
|
for i := range section.Children {
|
||||||
|
p.removeUnboundDMSBinds(§ion.Children[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) removeShadowedDMSBinds(section *HyprlandSection) {
|
||||||
|
counts := make(map[string]int)
|
||||||
|
p.countDMSBinds(section, counts)
|
||||||
|
p.filterShadowedDMSBinds(section, counts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) countDMSBinds(section *HyprlandSection, counts map[string]int) {
|
||||||
|
for i := range section.Keybinds {
|
||||||
|
kb := §ion.Keybinds[i]
|
||||||
|
if isDMSBindsSourcePath(kb.Source) {
|
||||||
|
counts[p.normalizeKey(p.formatBindKey(kb))]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range section.Children {
|
||||||
|
p.countDMSBinds(§ion.Children[i], counts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) filterShadowedDMSBinds(section *HyprlandSection, counts map[string]int) {
|
||||||
|
filtered := section.Keybinds[:0]
|
||||||
|
for i := range section.Keybinds {
|
||||||
|
kb := section.Keybinds[i]
|
||||||
|
key := p.normalizeKey(p.formatBindKey(&kb))
|
||||||
|
if isDMSBindsSourcePath(kb.Source) && counts[key] > 1 {
|
||||||
|
counts[key]--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, kb)
|
||||||
|
}
|
||||||
|
section.Keybinds = filtered
|
||||||
|
for i := range section.Children {
|
||||||
|
p.filterShadowedDMSBinds(§ion.Children[i], counts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *HyprlandParser) parseFileWithSource(filePath, sectionName string) (*HyprlandSection, error) {
|
func (p *HyprlandParser) parseFileWithSource(filePath, sectionName string) (*HyprlandSection, error) {
|
||||||
absPath, err := filepath.Abs(filePath)
|
absPath, err := filepath.Abs(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -407,6 +481,10 @@ func (p *HyprlandParser) parseFileWithSource(filePath, sectionName string) (*Hyp
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(filepath.Ext(absPath), ".lua") {
|
||||||
|
return p.parseLuaLines(string(data), filepath.Dir(absPath), absPath, sectionName)
|
||||||
|
}
|
||||||
|
|
||||||
prevSource := p.currentSource
|
prevSource := p.currentSource
|
||||||
p.currentSource = absPath
|
p.currentSource = absPath
|
||||||
|
|
||||||
@@ -446,7 +524,7 @@ func (p *HyprlandParser) handleSource(line string, section *HyprlandSection, bas
|
|||||||
}
|
}
|
||||||
|
|
||||||
sourcePath := strings.TrimSpace(parts[1])
|
sourcePath := strings.TrimSpace(parts[1])
|
||||||
isDMSSource := sourcePath == "dms/binds.conf" || strings.HasSuffix(sourcePath, "/dms/binds.conf")
|
isDMSSource := isDMSBindsPrimarySourcePath(sourcePath)
|
||||||
|
|
||||||
p.includeCount++
|
p.includeCount++
|
||||||
if isDMSSource {
|
if isDMSSource {
|
||||||
@@ -474,6 +552,17 @@ func (p *HyprlandParser) handleSource(line string, section *HyprlandSection, bas
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *HyprlandParser) parseDMSBindsDirectly(dmsBindsPath string, section *HyprlandSection) {
|
func (p *HyprlandParser) parseDMSBindsDirectly(dmsBindsPath string, section *HyprlandSection) {
|
||||||
|
if strings.EqualFold(filepath.Ext(dmsBindsPath), ".lua") {
|
||||||
|
sub, err := p.parseLuaLinesFromPath(dmsBindsPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
section.Keybinds = append(section.Keybinds, sub.Keybinds...)
|
||||||
|
section.Children = append(section.Children, sub.Children...)
|
||||||
|
p.dmsProcessed = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(dmsBindsPath)
|
data, err := os.ReadFile(dmsBindsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -503,6 +592,124 @@ func (p *HyprlandParser) parseDMSBindsDirectly(dmsBindsPath string, section *Hyp
|
|||||||
p.dmsProcessed = true
|
p.dmsProcessed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) parseLuaLinesFromPath(absPath string) (*HyprlandSection, error) {
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.parseLuaLines(string(data), filepath.Dir(absPath), absPath, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLuaLines reads a Hyprland Lua config fragment: require() includes and hl.bind keybinds.
|
||||||
|
func (p *HyprlandParser) parseLuaLines(content string, baseDir, absPath, sectionName string) (*HyprlandSection, error) {
|
||||||
|
section := &HyprlandSection{Name: sectionName}
|
||||||
|
prevSource := p.currentSource
|
||||||
|
p.currentSource = absPath
|
||||||
|
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
boundInFile := make(map[string]bool)
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" || strings.HasPrefix(trimmed, "--") || !strings.Contains(trimmed, "hl.bind") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if kbc, _, _, ok := parseLuaBindInvocation(trimmed); ok {
|
||||||
|
boundInFile[strings.ToLower(luaKeyComboToInternalKey(kbc))] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootDir := baseDir
|
||||||
|
if expanded, err := utils.ExpandPath(p.configDir); err == nil && expanded != "" {
|
||||||
|
rootDir = expanded
|
||||||
|
}
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" || strings.HasPrefix(trimmed, "--") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if modules := luaconfig.Requires(trimmed); len(modules) > 0 {
|
||||||
|
for _, mod := range modules {
|
||||||
|
rel := luaconfig.ModuleToRelPath(mod)
|
||||||
|
if rel == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
isDMS := isDMSBindsPrimarySourcePath(rel)
|
||||||
|
p.includeCount++
|
||||||
|
if isDMS {
|
||||||
|
p.dmsBindsIncluded = true
|
||||||
|
p.dmsIncludePos = p.includeCount
|
||||||
|
p.dmsProcessed = true
|
||||||
|
}
|
||||||
|
fullPath := luaconfig.ModuleToPath(rootDir, mod)
|
||||||
|
expanded, err := utils.ExpandPath(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
includedSection, err := p.parseFileWithSource(expanded, "")
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
section.Children = append(section.Children, *includedSection)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(trimmed, "hl.unbind") {
|
||||||
|
if key, ok := parseLuaUnbindLine(trimmed); ok {
|
||||||
|
normalized := strings.ToLower(key)
|
||||||
|
if !boundInFile[normalized] {
|
||||||
|
p.removedKeys[normalized] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(trimmed, "hl.bind") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
kbc, action, optSuffix, ok := parseLuaBindInvocation(trimmed)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
flags := luaBindOptFlags(optSuffix)
|
||||||
|
desc := luaBindOptDescription(optSuffix)
|
||||||
|
if desc == "" {
|
||||||
|
desc = luaLineTrailingComment(line)
|
||||||
|
}
|
||||||
|
kb := luaKeyComboToBinding(kbc, action, p.currentSource, desc)
|
||||||
|
kb.Flags = flags
|
||||||
|
if p.addBind(kb) {
|
||||||
|
section.Keybinds = append(section.Keybinds, *kb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.currentSource = prevSource
|
||||||
|
return section, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaBindOptFlags(optSuffix string) string {
|
||||||
|
optSuffix = strings.TrimSpace(optSuffix)
|
||||||
|
if optSuffix == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var flags string
|
||||||
|
if strings.Contains(optSuffix, "repeating") {
|
||||||
|
flags += "e"
|
||||||
|
}
|
||||||
|
if strings.Contains(optSuffix, "locked") {
|
||||||
|
flags += "l"
|
||||||
|
}
|
||||||
|
if strings.Contains(optSuffix, "description") {
|
||||||
|
flags += "d"
|
||||||
|
}
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaBindOptDescription(optSuffix string) string {
|
||||||
|
return luaTableStringField(optSuffix, "description")
|
||||||
|
}
|
||||||
|
|
||||||
func (p *HyprlandParser) parseBindLine(line string) *HyprlandKeyBinding {
|
func (p *HyprlandParser) parseBindLine(line string) *HyprlandKeyBinding {
|
||||||
parts := strings.SplitN(line, "=", 2)
|
parts := strings.SplitN(line, "=", 2)
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
@@ -623,5 +830,356 @@ func ParseHyprlandKeysWithDMS(path string) (*HyprlandParseResult, error) {
|
|||||||
DMSBindsIncluded: parser.dmsBindsIncluded,
|
DMSBindsIncluded: parser.dmsBindsIncluded,
|
||||||
DMSStatus: parser.buildDMSStatus(),
|
DMSStatus: parser.buildDMSStatus(),
|
||||||
ConflictingConfigs: parser.conflictingConfigs,
|
ConflictingConfigs: parser.conflictingConfigs,
|
||||||
|
DefaultDMSKeys: parser.defaultDMSKeys,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func skipLuaWS(s string, i int) int {
|
||||||
|
for i < len(s) && (s[i] == ' ' || s[i] == '\t' || s[i] == '\r') {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLuaStringLiteral reads a Lua "..." or '...' starting at i (first quote).
|
||||||
|
func parseLuaStringLiteral(line string, i int) (value string, next int, ok bool) {
|
||||||
|
if i >= len(line) {
|
||||||
|
return "", i, false
|
||||||
|
}
|
||||||
|
q := line[i]
|
||||||
|
if q != '"' && q != '\'' {
|
||||||
|
return "", i, false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
var sb strings.Builder
|
||||||
|
for i < len(line) {
|
||||||
|
c := line[i]
|
||||||
|
if c == '\\' && i+1 < len(line) {
|
||||||
|
i++
|
||||||
|
sb.WriteByte(line[i])
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c == q {
|
||||||
|
return sb.String(), i + 1, true
|
||||||
|
}
|
||||||
|
sb.WriteByte(c)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return "", i, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLuaFirstArgExpr parses a single Lua expression starting at i, stopping when parentheses
|
||||||
|
// opened from the first '(' are balanced (handles nested () and {} and double-quoted strings).
|
||||||
|
func parseLuaFirstArgExpr(line string, start int) (expr string, next int, ok bool) {
|
||||||
|
start = skipLuaWS(line, start)
|
||||||
|
if start >= len(line) {
|
||||||
|
return "", start, false
|
||||||
|
}
|
||||||
|
// Find first '(' of the call (e.g. hl.dsp.exec_cmd(...)
|
||||||
|
firstParen := strings.IndexByte(line[start:], '(')
|
||||||
|
if firstParen < 0 {
|
||||||
|
return "", start, false
|
||||||
|
}
|
||||||
|
i := start + firstParen
|
||||||
|
depth := 0
|
||||||
|
inStr := byte(0)
|
||||||
|
esc := false
|
||||||
|
exprStart := start
|
||||||
|
for ; i < len(line); i++ {
|
||||||
|
c := line[i]
|
||||||
|
if inStr != 0 {
|
||||||
|
if esc {
|
||||||
|
esc = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c == '\\' && inStr == '"' {
|
||||||
|
esc = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c == inStr {
|
||||||
|
inStr = 0
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch c {
|
||||||
|
case '"', '\'':
|
||||||
|
inStr = c
|
||||||
|
case '(':
|
||||||
|
depth++
|
||||||
|
case ')':
|
||||||
|
depth--
|
||||||
|
if depth == 0 {
|
||||||
|
return strings.TrimSpace(line[exprStart : i+1]), i + 1, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", start, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLuaBindInvocation parses one hl.bind("KEY", expr [, opts]) on a single line.
|
||||||
|
func parseLuaBindInvocation(line string) (keyCombo, actionExpr, optSuffix string, ok bool) {
|
||||||
|
idx := strings.Index(line, "hl.bind")
|
||||||
|
if idx < 0 {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
i := idx + len("hl.bind")
|
||||||
|
i = skipLuaWS(line, i)
|
||||||
|
if i >= len(line) || line[i] != '(' {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
i = skipLuaWS(line, i)
|
||||||
|
keyCombo, i, ok = parseLuaStringLiteral(line, i)
|
||||||
|
if !ok {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
i = skipLuaWS(line, i)
|
||||||
|
if i >= len(line) || line[i] != ',' {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
i = skipLuaWS(line, i)
|
||||||
|
actionExpr, i, ok = parseLuaFirstArgExpr(line, i)
|
||||||
|
if !ok {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
i = skipLuaWS(line, i)
|
||||||
|
if i < len(line) && line[i] == ',' {
|
||||||
|
optSuffix = strings.TrimSpace(line[i:])
|
||||||
|
}
|
||||||
|
return keyCombo, strings.TrimSpace(actionExpr), optSuffix, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaKeyComboToBinding(keyCombo, actionExpr, source, lineComment string) *HyprlandKeyBinding {
|
||||||
|
keyCombo = strings.TrimSpace(keyCombo)
|
||||||
|
mods, leaf := luaKeyComboToModsKey(keyCombo)
|
||||||
|
dispatcher, params := luaExprToDispatcherParams(actionExpr)
|
||||||
|
comment := lineComment
|
||||||
|
if comment == "" {
|
||||||
|
comment = hyprlandAutogenerateComment(dispatcher, params)
|
||||||
|
}
|
||||||
|
return &HyprlandKeyBinding{
|
||||||
|
Mods: mods,
|
||||||
|
Key: leaf,
|
||||||
|
Dispatcher: dispatcher,
|
||||||
|
Params: params,
|
||||||
|
Comment: comment,
|
||||||
|
Source: source,
|
||||||
|
Flags: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaKeyComboToModsKey(combo string) (mods []string, leaf string) {
|
||||||
|
parts := strings.Split(combo, "+")
|
||||||
|
for i := range parts {
|
||||||
|
parts[i] = strings.TrimSpace(parts[i])
|
||||||
|
}
|
||||||
|
switch len(parts) {
|
||||||
|
case 0:
|
||||||
|
return nil, ""
|
||||||
|
case 1:
|
||||||
|
return nil, parts[0]
|
||||||
|
default:
|
||||||
|
return parts[:len(parts)-1], parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaExprToDispatcherParams(expr string) (dispatcher, params string) {
|
||||||
|
expr = strings.TrimSpace(expr)
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(expr, "hl.dsp.exec_cmd("):
|
||||||
|
arg := extractLuaCallStringArg(expr, "hl.dsp.exec_cmd")
|
||||||
|
if arg != "" {
|
||||||
|
if u, err := strconv.Unquote(arg); err == nil {
|
||||||
|
if strings.HasPrefix(u, "hyprctl dispatch ") {
|
||||||
|
rest := strings.TrimSpace(strings.TrimPrefix(u, "hyprctl dispatch "))
|
||||||
|
parts := strings.SplitN(rest, " ", 2)
|
||||||
|
if len(parts) == 1 {
|
||||||
|
return parts[0], ""
|
||||||
|
}
|
||||||
|
return parts[0], parts[1]
|
||||||
|
}
|
||||||
|
return "exec", u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "exec", strings.TrimSpace(strings.TrimPrefix(expr, "hl.dsp.exec_cmd"))
|
||||||
|
case strings.Contains(expr, "hl.dsp.window.kill()"):
|
||||||
|
return "killactive", ""
|
||||||
|
case strings.HasPrefix(expr, "hl.dsp.window.fullscreen("):
|
||||||
|
switch luaTableStringField(expr, "mode") {
|
||||||
|
case "maximized", "maximize":
|
||||||
|
return "fullscreen", "1"
|
||||||
|
case "fullscreen":
|
||||||
|
return "fullscreen", "0"
|
||||||
|
}
|
||||||
|
return "fullscreen", luaTableStringField(expr, "mode")
|
||||||
|
case strings.HasPrefix(expr, "hl.dsp.window.float("):
|
||||||
|
return "togglefloating", ""
|
||||||
|
case strings.Contains(expr, "hl.dsp.group.toggle()"):
|
||||||
|
return "togglegroup", ""
|
||||||
|
case strings.HasPrefix(expr, "hl.dsp.focus("):
|
||||||
|
switch {
|
||||||
|
case luaTableStringField(expr, "direction") != "":
|
||||||
|
return "movefocus", luaTableStringField(expr, "direction")
|
||||||
|
case luaTableStringField(expr, "monitor") != "":
|
||||||
|
return "focusmonitor", luaTableStringField(expr, "monitor")
|
||||||
|
case luaTableStringField(expr, "workspace") != "":
|
||||||
|
return "workspace", luaTableStringField(expr, "workspace")
|
||||||
|
case luaTableStringField(expr, "window") != "":
|
||||||
|
return "focuswindow", luaTableStringField(expr, "window")
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(expr, "hl.dsp.window.move("):
|
||||||
|
switch {
|
||||||
|
case luaTableStringField(expr, "direction") != "":
|
||||||
|
return "movewindow", luaTableStringField(expr, "direction")
|
||||||
|
case luaTableStringField(expr, "monitor") != "":
|
||||||
|
return "movewindow", "mon:" + luaTableStringField(expr, "monitor")
|
||||||
|
case luaTableStringField(expr, "workspace") != "":
|
||||||
|
return "movetoworkspace", luaTableStringField(expr, "workspace")
|
||||||
|
}
|
||||||
|
case expr == "hl.dsp.window.drag()":
|
||||||
|
return "movewindow", ""
|
||||||
|
case expr == "hl.dsp.window.resize()":
|
||||||
|
return "resizewindow", ""
|
||||||
|
case strings.HasPrefix(expr, "hl.dsp.window.resize("):
|
||||||
|
x := luaStringValue(luaTableScalarField(expr, "x"))
|
||||||
|
y := luaStringValue(luaTableScalarField(expr, "y"))
|
||||||
|
if x != "" || y != "" {
|
||||||
|
if x == "" {
|
||||||
|
x = "0"
|
||||||
|
}
|
||||||
|
if y == "" {
|
||||||
|
y = "0"
|
||||||
|
}
|
||||||
|
return "resizeactive", x + " " + y
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(expr, "hl.dsp.layout("):
|
||||||
|
arg := extractLuaCallStringArg(expr, "hl.dsp.layout")
|
||||||
|
if arg != "" {
|
||||||
|
if u, err := strconv.Unquote(arg); err == nil {
|
||||||
|
return "layoutmsg", u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(expr, "hl.dsp.dpms("):
|
||||||
|
if action := luaTableStringField(expr, "action"); action != "" {
|
||||||
|
return "dpms", action
|
||||||
|
}
|
||||||
|
case strings.Contains(expr, "hl.dsp.exit()"):
|
||||||
|
return "exit", ""
|
||||||
|
default:
|
||||||
|
return "exec", "hyprctl dispatch lua:" + expr
|
||||||
|
}
|
||||||
|
return "exec", "hyprctl dispatch lua:" + expr
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractLuaCallStringArg(callExpr, funcName string) string {
|
||||||
|
callExpr = strings.TrimSpace(callExpr)
|
||||||
|
prefix := funcName + "("
|
||||||
|
if !strings.HasPrefix(callExpr, prefix) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
inner := callExpr[len(prefix):]
|
||||||
|
inner = strings.TrimSpace(inner)
|
||||||
|
if len(inner) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch inner[0] {
|
||||||
|
case '"', '\'':
|
||||||
|
s, _, ok := parseLuaStringLiteral(inner, 0)
|
||||||
|
if ok {
|
||||||
|
return strconv.Quote(s)
|
||||||
|
}
|
||||||
|
case '[':
|
||||||
|
if strings.HasPrefix(inner, "[[") {
|
||||||
|
if end := strings.Index(inner[2:], "]]"); end >= 0 {
|
||||||
|
return strconv.Quote(inner[2 : 2+end])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaTableStringField(expr, field string) string {
|
||||||
|
return luaStringValue(luaTableScalarField(expr, field))
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaTableScalarField(expr, field string) string {
|
||||||
|
re := regexp.MustCompile(`(?s)\b` + regexp.QuoteMeta(field) + `\s*=\s*("(?:\\.|[^"])*"|'(?:\\.|[^'])*'|\[\[.*?\]\]|-?\d+(?:\.\d+)?|true|false)`)
|
||||||
|
m := re.FindStringSubmatch(expr)
|
||||||
|
if len(m) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(m[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaStringValue(raw string) string {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(raw, "[[") && strings.HasSuffix(raw, "]]") {
|
||||||
|
return raw[2 : len(raw)-2]
|
||||||
|
}
|
||||||
|
if len(raw) >= 2 {
|
||||||
|
q := raw[0]
|
||||||
|
if (q == '"' || q == '\'') && raw[len(raw)-1] == q {
|
||||||
|
if q == '"' {
|
||||||
|
if u, err := strconv.Unquote(raw); err == nil {
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.ReplaceAll(raw[1:len(raw)-1], `\'`, `'`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaLineTrailingComment(line string) string {
|
||||||
|
if idx := strings.Index(line, "--"); idx >= 0 {
|
||||||
|
return strings.TrimSpace(line[idx+2:])
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDMSBindsSourcePath(p string) bool {
|
||||||
|
p = filepath.ToSlash(strings.TrimSpace(p))
|
||||||
|
if isDMSBindsPrimarySourcePath(p) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return isDMSBindsUserOverridePath(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDMSBindsUserOverridePath(p string) bool {
|
||||||
|
p = filepath.ToSlash(strings.TrimSpace(p))
|
||||||
|
return p == "dms/binds-user.lua" || p == "./dms/binds-user.lua" ||
|
||||||
|
strings.HasSuffix(p, "/dms/binds-user.lua")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDMSBindsPrimarySourcePath(p string) bool {
|
||||||
|
p = filepath.ToSlash(strings.TrimSpace(p))
|
||||||
|
if strings.Contains(p, "/dms/binds.lua") || strings.HasSuffix(p, "dms/binds.lua") || p == "dms/binds.lua" || p == "./dms/binds.lua" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.Contains(p, "/dms/binds.conf") || strings.HasSuffix(p, "dms/binds.conf") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return p == "dms/binds.conf" || p == "./dms/binds.conf"
|
||||||
|
}
|
||||||
|
|
||||||
|
// hyprlandMainConfigPath returns hyprland.lua if present, else hyprland.conf if present.
|
||||||
|
func hyprlandMainConfigPath(dir string) (string, error) {
|
||||||
|
expandedDir, err := utils.ExpandPath(dir)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
luaPath := filepath.Join(expandedDir, "hyprland.lua")
|
||||||
|
if st, err := os.Stat(luaPath); err == nil && st.Mode().IsRegular() {
|
||||||
|
return luaPath, nil
|
||||||
|
}
|
||||||
|
confPath := filepath.Join(expandedDir, "hyprland.conf")
|
||||||
|
if st, err := os.Stat(confPath); err == nil && st.Mode().IsRegular() {
|
||||||
|
return confPath, nil
|
||||||
|
}
|
||||||
|
return "", os.ErrNotExist
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package providers
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHyprlandAutogenerateComment(t *testing.T) {
|
func TestHyprlandAutogenerateComment(t *testing.T) {
|
||||||
@@ -60,6 +63,341 @@ func TestHyprlandAutogenerateComment(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHyprlandLuaBindRoundTripHelpers(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
expr string
|
||||||
|
wantDispatcher string
|
||||||
|
wantParams string
|
||||||
|
}{
|
||||||
|
{`hl.dsp.exec_cmd([[dms ipc call brightness increment 5 ""]])`, "exec", `dms ipc call brightness increment 5 ""`},
|
||||||
|
{`hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" })`, "fullscreen", "1"},
|
||||||
|
{`hl.dsp.focus({ workspace = "e+1" })`, "workspace", "e+1"},
|
||||||
|
{`hl.dsp.window.move({ monitor = "l" })`, "movewindow", "mon:l"},
|
||||||
|
{`hl.dsp.window.resize({ x = "-10%", y = 0, relative = true })`, "resizeactive", "-10% 0"},
|
||||||
|
{`hl.dsp.layout("togglesplit")`, "layoutmsg", "togglesplit"},
|
||||||
|
{`hl.dsp.dpms({ action = "toggle" })`, "dpms", "toggle"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.expr, func(t *testing.T) {
|
||||||
|
gotDispatcher, gotParams := luaExprToDispatcherParams(tt.expr)
|
||||||
|
if gotDispatcher != tt.wantDispatcher || gotParams != tt.wantParams {
|
||||||
|
t.Fatalf("luaExprToDispatcherParams() = %q, %q; want %q, %q", gotDispatcher, gotParams, tt.wantDispatcher, tt.wantParams)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteLuaBindLineOptionsInsideCall(t *testing.T) {
|
||||||
|
var sb strings.Builder
|
||||||
|
writeLuaBindLine(&sb, &hyprlandOverrideBind{
|
||||||
|
Key: "Super+k",
|
||||||
|
Action: "exec kitty",
|
||||||
|
Description: "Open terminal",
|
||||||
|
Flags: "led",
|
||||||
|
})
|
||||||
|
|
||||||
|
want := `hl.unbind("SUPER + K")
|
||||||
|
hl.bind("SUPER + K", hl.dsp.exec_cmd("kitty"), { locked = true, repeating = true, description = "Open terminal" })`
|
||||||
|
if got := strings.TrimSpace(sb.String()); got != want {
|
||||||
|
t.Fatalf("writeLuaBindLine() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteLuaBindLineMapsSpawnActionForHyprland(t *testing.T) {
|
||||||
|
var sb strings.Builder
|
||||||
|
writeLuaBindLine(&sb, &hyprlandOverrideBind{
|
||||||
|
Key: "Super+n",
|
||||||
|
Action: "spawn dms ipc call notepad toggle",
|
||||||
|
Description: "Notepad: Toggle",
|
||||||
|
})
|
||||||
|
|
||||||
|
want := `hl.unbind("SUPER + N")
|
||||||
|
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notepad toggle")) -- Notepad: Toggle`
|
||||||
|
if got := strings.TrimSpace(sb.String()); got != want {
|
||||||
|
t.Fatalf("writeLuaBindLine() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandLuaBindsUserOverridesDefaults(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.lua"), []byte(`
|
||||||
|
require("dms.binds")
|
||||||
|
require("dms.binds-user")
|
||||||
|
`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds.lua"), []byte(`hl.bind("SUPER + T", hl.dsp.exec_cmd("kitty"))`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte(`hl.bind("SUPER + T", hl.dsp.exec_cmd("foot"), { description = "User terminal" })`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseHyprlandKeysWithDMS(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var found []HyprlandKeyBinding
|
||||||
|
var walk func(HyprlandSection)
|
||||||
|
walk = func(section HyprlandSection) {
|
||||||
|
for _, kb := range section.Keybinds {
|
||||||
|
if strings.EqualFold(strings.Join(append(kb.Mods, kb.Key), "+"), "SUPER+T") {
|
||||||
|
found = append(found, kb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, child := range section.Children {
|
||||||
|
walk(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(*result.Section)
|
||||||
|
|
||||||
|
if len(found) != 1 {
|
||||||
|
t.Fatalf("expected one effective SUPER+T bind, got %d: %#v", len(found), found)
|
||||||
|
}
|
||||||
|
if found[0].Params != "foot" || found[0].Comment != "User terminal" {
|
||||||
|
t.Fatalf("expected user override bind, got %#v", found[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteLuaBindLineEmitsUnbindOnlyForNegativeOverride(t *testing.T) {
|
||||||
|
var sb strings.Builder
|
||||||
|
writeLuaBindLine(&sb, &hyprlandOverrideBind{Key: "Super+i", Unbind: true})
|
||||||
|
|
||||||
|
want := `hl.unbind("SUPER + I")`
|
||||||
|
if got := strings.TrimSpace(sb.String()); got != want {
|
||||||
|
t.Fatalf("writeLuaBindLine() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadLuaOverrideRecognizesLoneUnbindAsNegativeOverride(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
overridePath := filepath.Join(tmpDir, "binds-user.lua")
|
||||||
|
contents := `-- DMS user keybind overrides
|
||||||
|
hl.unbind("SUPER + I")
|
||||||
|
hl.unbind("SUPER + N")
|
||||||
|
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(overridePath, []byte(contents), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
binds, err := readLuaOrHyprlangOverride(overridePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, ok := binds["super+i"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected SUPER+I entry in override map, got: %#v", binds)
|
||||||
|
}
|
||||||
|
if !got.Unbind {
|
||||||
|
t.Fatalf("expected SUPER+I to be marked Unbind, got: %#v", got)
|
||||||
|
}
|
||||||
|
if rebind, ok := binds["super+n"]; !ok || rebind.Unbind {
|
||||||
|
t.Fatalf("expected SUPER+N to be a normal rebind override, got: %#v", rebind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParserDropsDMSDefaultsSuppressedByBindsUserUnbind(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.lua"), []byte(`
|
||||||
|
require("dms.binds")
|
||||||
|
require("dms.binds-user")
|
||||||
|
`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds.lua"), []byte(
|
||||||
|
`hl.bind("SUPER + I", hl.dsp.focus({ workspace = "e-1" }))
|
||||||
|
hl.bind("SUPER + T", hl.dsp.exec_cmd("kitty"))`,
|
||||||
|
), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte(`hl.unbind("SUPER + I")`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseHyprlandKeysWithDMS(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys []string
|
||||||
|
var walk func(HyprlandSection)
|
||||||
|
walk = func(section HyprlandSection) {
|
||||||
|
for _, kb := range section.Keybinds {
|
||||||
|
keys = append(keys, strings.ToUpper(strings.Join(append(kb.Mods, kb.Key), "+")))
|
||||||
|
}
|
||||||
|
for _, child := range section.Children {
|
||||||
|
walk(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(*result.Section)
|
||||||
|
|
||||||
|
for _, k := range keys {
|
||||||
|
if k == "SUPER+I" {
|
||||||
|
t.Fatalf("expected SUPER+I to be suppressed by binds-user.lua unbind, got: %v", keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foundT := false
|
||||||
|
for _, k := range keys {
|
||||||
|
if k == "SUPER+T" {
|
||||||
|
foundT = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundT {
|
||||||
|
t.Fatalf("expected SUPER+T to remain (only SUPER+I was unbound), got: %v", keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandRemoveBindWritesNegativeOverrideForDefault(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := NewHyprlandProvider(tmpDir)
|
||||||
|
if err := provider.RemoveBind("SUPER+I"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(data), `hl.unbind("SUPER + I")`) {
|
||||||
|
t.Fatalf("expected negative override hl.unbind line, got:\n%s", string(data))
|
||||||
|
}
|
||||||
|
if strings.Contains(string(data), `hl.bind("SUPER + I"`) {
|
||||||
|
t.Fatalf("expected NO hl.bind for SUPER+I, got:\n%s", string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandRemoveBindReplacesExistingOverrideWithNegativeOverride(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
override := `hl.unbind("SUPER + N")
|
||||||
|
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte(override), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := NewHyprlandProvider(tmpDir)
|
||||||
|
if err := provider.RemoveBind("SUPER+N"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(data), `hl.unbind("SUPER + N")`) {
|
||||||
|
t.Fatalf("expected negative override hl.unbind line, got:\n%s", string(data))
|
||||||
|
}
|
||||||
|
if strings.Contains(string(data), `hl.bind("SUPER + N"`) {
|
||||||
|
t.Fatalf("expected NO hl.bind for SUPER+N after remove, got:\n%s", string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandResetBindRevertsExistingOverrideToDefault(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
override := `hl.unbind("SUPER + N")
|
||||||
|
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte(override), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := NewHyprlandProvider(tmpDir)
|
||||||
|
if err := provider.ResetBind("SUPER+N"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if strings.Contains(string(data), `SUPER + N`) {
|
||||||
|
t.Fatalf("expected SUPER+N to be fully removed (revert to default), got:\n%s", string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandHasDefaultSetForOverrideOfDefaultKey(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.lua"), []byte(`
|
||||||
|
require("dms.binds")
|
||||||
|
require("dms.binds-user")
|
||||||
|
`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds.lua"), []byte(
|
||||||
|
`hl.bind("SUPER + T", hl.dsp.exec_cmd("kitty"))`,
|
||||||
|
), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte(
|
||||||
|
`hl.unbind("SUPER + T")
|
||||||
|
hl.bind("SUPER + T", hl.dsp.exec_cmd("foot"))
|
||||||
|
hl.bind("SUPER + Z", hl.dsp.exec_cmd("custom"))`,
|
||||||
|
), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := NewHyprlandProvider(tmpDir)
|
||||||
|
sheet, err := provider.GetCheatSheet()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var foundT, foundZ *keybinds.Keybind
|
||||||
|
for _, group := range sheet.Binds {
|
||||||
|
for i := range group {
|
||||||
|
kb := group[i]
|
||||||
|
keyUpper := strings.ToUpper(kb.Key)
|
||||||
|
if keyUpper == "SUPER+T" {
|
||||||
|
foundT = &group[i]
|
||||||
|
}
|
||||||
|
if keyUpper == "SUPER+Z" {
|
||||||
|
foundZ = &group[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if foundT == nil {
|
||||||
|
t.Fatalf("expected SUPER+T override in cheatsheet")
|
||||||
|
}
|
||||||
|
if !foundT.HasDefault {
|
||||||
|
t.Fatalf("expected SUPER+T HasDefault=true (default exists in binds.lua), got %+v", foundT)
|
||||||
|
}
|
||||||
|
if foundZ == nil {
|
||||||
|
t.Fatalf("expected SUPER+Z (user-only) in cheatsheet")
|
||||||
|
}
|
||||||
|
if foundZ.HasDefault {
|
||||||
|
t.Fatalf("expected SUPER+Z HasDefault=false (no default), got %+v", foundZ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHyprlandGetKeybindAtLine(t *testing.T) {
|
func TestHyprlandGetKeybindAtLine(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding, conflicts map[st
|
|||||||
|
|
||||||
source := "config"
|
source := "config"
|
||||||
if strings.Contains(kb.Source, "dms/binds.conf") || strings.Contains(kb.Source, "dms"+string(filepath.Separator)+"binds.conf") {
|
if strings.Contains(kb.Source, "dms/binds.conf") || strings.Contains(kb.Source, "dms"+string(filepath.Separator)+"binds.conf") {
|
||||||
source = "dms"
|
source = "dms-default"
|
||||||
}
|
}
|
||||||
|
|
||||||
bind := keybinds.Keybind{
|
bind := keybinds.Keybind{
|
||||||
@@ -151,7 +151,7 @@ func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding, conflicts map[st
|
|||||||
Source: source,
|
Source: source,
|
||||||
}
|
}
|
||||||
|
|
||||||
if source == "dms" && conflicts != nil {
|
if source == "dms-default" && conflicts != nil {
|
||||||
normalizedKey := strings.ToLower(keyStr)
|
normalizedKey := strings.ToLower(keyStr)
|
||||||
if conflictKb, ok := conflicts[normalizedKey]; ok {
|
if conflictKb, ok := conflicts[normalizedKey]; ok {
|
||||||
bind.Conflict = &keybinds.Keybind{
|
bind.Conflict = &keybinds.Keybind{
|
||||||
@@ -249,6 +249,10 @@ func (m *MangoWCProvider) RemoveBind(key string) error {
|
|||||||
return m.writeOverrideBinds(existingBinds)
|
return m.writeOverrideBinds(existingBinds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) ResetBind(key string) error {
|
||||||
|
return m.RemoveBind(key)
|
||||||
|
}
|
||||||
|
|
||||||
type mangowcOverrideBind struct {
|
type mangowcOverrideBind struct {
|
||||||
Key string
|
Key string
|
||||||
Action string
|
Action string
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, co
|
|||||||
|
|
||||||
source := "config"
|
source := "config"
|
||||||
if strings.Contains(kb.Source, "dms/binds.kdl") {
|
if strings.Contains(kb.Source, "dms/binds.kdl") {
|
||||||
source = "dms"
|
source = "dms-default"
|
||||||
}
|
}
|
||||||
|
|
||||||
bind := keybinds.Keybind{
|
bind := keybinds.Keybind{
|
||||||
@@ -165,7 +165,7 @@ func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, co
|
|||||||
Repeat: kb.Repeat,
|
Repeat: kb.Repeat,
|
||||||
}
|
}
|
||||||
|
|
||||||
if source == "dms" && conflicts != nil {
|
if source == "dms-default" && conflicts != nil {
|
||||||
if conflictKb, ok := conflicts[keyStr]; ok {
|
if conflictKb, ok := conflicts[keyStr]; ok {
|
||||||
bind.Conflict = &keybinds.Keybind{
|
bind.Conflict = &keybinds.Keybind{
|
||||||
Key: keyStr,
|
Key: keyStr,
|
||||||
@@ -269,6 +269,10 @@ func (n *NiriProvider) RemoveBind(key string) error {
|
|||||||
return n.writeOverrideBinds(existingBinds)
|
return n.writeOverrideBinds(existingBinds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) ResetBind(key string) error {
|
||||||
|
return n.RemoveBind(key)
|
||||||
|
}
|
||||||
|
|
||||||
type overrideBind struct {
|
type overrideBind struct {
|
||||||
Key string
|
Key string
|
||||||
Action string
|
Action string
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Keybind struct {
|
|||||||
AllowInhibiting *bool `json:"allowInhibiting,omitempty"` // nil=default(true), false=explicitly disabled
|
AllowInhibiting *bool `json:"allowInhibiting,omitempty"` // nil=default(true), false=explicitly disabled
|
||||||
Repeat *bool `json:"repeat,omitempty"` // nil=default(true), false=explicitly disabled
|
Repeat *bool `json:"repeat,omitempty"` // nil=default(true), false=explicitly disabled
|
||||||
Conflict *Keybind `json:"conflict,omitempty"`
|
Conflict *Keybind `json:"conflict,omitempty"`
|
||||||
|
HasDefault bool `json:"hasDefault,omitempty"` // override has a DMS default to revert to
|
||||||
}
|
}
|
||||||
|
|
||||||
type DMSBindsStatus struct {
|
type DMSBindsStatus struct {
|
||||||
@@ -42,6 +43,11 @@ type Provider interface {
|
|||||||
type WritableProvider interface {
|
type WritableProvider interface {
|
||||||
Provider
|
Provider
|
||||||
SetBind(key, action, description string, options map[string]any) error
|
SetBind(key, action, description string, options map[string]any) error
|
||||||
|
// RemoveBind removes the bind. Hyprland writes a negative override to
|
||||||
|
// dms/binds-user.lua; single-file providers delete the line.
|
||||||
RemoveBind(key string) error
|
RemoveBind(key string) error
|
||||||
|
// ResetBind reverts a user override to its DMS default. On single-file
|
||||||
|
// providers this aliases to RemoveBind.
|
||||||
|
ResetBind(key string) error
|
||||||
GetOverridePath() string
|
GetOverridePath() string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package luaconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var luaRequireRE = regexp.MustCompile(`(?i)\brequire\s*\(\s*["']([^"']+)["']\s*\)`)
|
||||||
|
|
||||||
|
func ModuleToRelPath(module string) string {
|
||||||
|
module = strings.TrimSpace(module)
|
||||||
|
if module == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
module = strings.NewReplacer(".", string(filepath.Separator), "/", string(filepath.Separator)).Replace(module)
|
||||||
|
return filepath.Clean(module + ".lua")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ModuleToPath(baseDir, module string) string {
|
||||||
|
rel := ModuleToRelPath(module)
|
||||||
|
if rel == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filepath.Clean(filepath.Join(baseDir, rel))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Requires(line string) []string {
|
||||||
|
line = stripLineComment(line)
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
matches := luaRequireRE.FindAllStringSubmatch(line, -1)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
modules := make([]string, 0, len(matches))
|
||||||
|
for _, match := range matches {
|
||||||
|
if len(match) > 1 && strings.TrimSpace(match[1]) != "" {
|
||||||
|
modules = append(modules, strings.TrimSpace(match[1]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modules
|
||||||
|
}
|
||||||
|
|
||||||
|
func Require(line string) (string, bool) {
|
||||||
|
modules := Requires(line)
|
||||||
|
if len(modules) != 1 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return modules[0], true
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequiresTarget(filePath, targetAbs string, processed map[string]bool) bool {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return requiresTarget(absPath, filepath.Dir(absPath), targetAbs, processed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requiresTarget(filePath, rootDir, targetAbs string, processed map[string]bool) bool {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
targetAbsClean := filepath.Clean(targetAbs)
|
||||||
|
|
||||||
|
if processed[absPath] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
processed[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, raw := range strings.Split(string(data), "\n") {
|
||||||
|
for _, module := range Requires(raw) {
|
||||||
|
candidate := ModuleToPath(rootDir, module)
|
||||||
|
if candidate == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if filepath.Clean(candidate) == targetAbsClean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
|
||||||
|
if requiresTarget(candidate, rootDir, targetAbs, processed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripLineComment(line string) string {
|
||||||
|
inStr := byte(0)
|
||||||
|
esc := false
|
||||||
|
for i := 0; i+1 < len(line); i++ {
|
||||||
|
c := line[i]
|
||||||
|
if inStr != 0 {
|
||||||
|
if esc {
|
||||||
|
esc = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c == '\\' && inStr == '"' {
|
||||||
|
esc = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c == inStr {
|
||||||
|
inStr = 0
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch c {
|
||||||
|
case '"', '\'':
|
||||||
|
inStr = c
|
||||||
|
case '-':
|
||||||
|
if line[i+1] == '-' {
|
||||||
|
return line[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package luaconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestModuleToRelPath(t *testing.T) {
|
||||||
|
tests := map[string]string{
|
||||||
|
"dms.binds": filepath.Join("dms", "binds.lua"),
|
||||||
|
"dms/binds-user": filepath.Join("dms", "binds-user.lua"),
|
||||||
|
"awesome/anim": filepath.Join("awesome", "anim.lua"),
|
||||||
|
"awesome.colors": filepath.Join("awesome", "colors.lua"),
|
||||||
|
" awesome.binds ": filepath.Join("awesome", "binds.lua"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for input, want := range tests {
|
||||||
|
if got := ModuleToRelPath(input); got != want {
|
||||||
|
t.Fatalf("ModuleToRelPath(%q) = %q, want %q", input, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiresSkipsComments(t *testing.T) {
|
||||||
|
if modules := Requires(`-- require("dms.binds")`); len(modules) != 0 {
|
||||||
|
t.Fatalf("expected commented require to be ignored, got %#v", modules)
|
||||||
|
}
|
||||||
|
|
||||||
|
modules := Requires(`print("-- not a comment") require("dms.binds") -- require("ignored")`)
|
||||||
|
if len(modules) != 1 || modules[0] != "dms.binds" {
|
||||||
|
t.Fatalf("unexpected modules: %#v", modules)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiresTargetRecurses(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
target := filepath.Join(dmsDir, "windowrules.lua")
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.lua"), []byte(`require("dms.extra")`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "extra.lua"), []byte(`require("dms.windowrules")`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(target, []byte(`-- rules`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !RequiresTarget(filepath.Join(tmpDir, "hyprland.lua"), target, make(map[string]bool)) {
|
||||||
|
t.Fatal("expected recursive require lookup to find target")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -300,9 +300,14 @@ func (m Model) checkExistingConfigurations() tea.Cmd {
|
|||||||
Exists: niriExists,
|
Exists: niriExists,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
hyprlandPath := filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf")
|
hyprlandLuaPath := filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.lua")
|
||||||
|
hyprlandConfPath := filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf")
|
||||||
|
hyprlandPath := hyprlandLuaPath
|
||||||
hyprlandExists := false
|
hyprlandExists := false
|
||||||
if _, err := os.Stat(hyprlandPath); err == nil {
|
if _, err := os.Stat(hyprlandLuaPath); err == nil {
|
||||||
|
hyprlandExists = true
|
||||||
|
} else if _, err := os.Stat(hyprlandConfPath); err == nil {
|
||||||
|
hyprlandPath = hyprlandConfPath
|
||||||
hyprlandExists = true
|
hyprlandExists = true
|
||||||
}
|
}
|
||||||
configs = append(configs, ExistingConfigInfo{
|
configs = append(configs, ExistingConfigInfo{
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,10 @@ package providers
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseWindowRuleV1(t *testing.T) {
|
func TestParseWindowRuleV1(t *testing.T) {
|
||||||
@@ -151,7 +154,7 @@ func TestHyprlandWritableProvider(t *testing.T) {
|
|||||||
t.Errorf("Name() = %q, want hyprland", provider.Name())
|
t.Errorf("Name() = %q, want hyprland", provider.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedPath := filepath.Join(tmpDir, "dms", "windowrules.conf")
|
expectedPath := filepath.Join(tmpDir, "dms", "windowrules.lua")
|
||||||
if provider.GetOverridePath() != expectedPath {
|
if provider.GetOverridePath() != expectedPath {
|
||||||
t.Errorf("GetOverridePath() = %q, want %q", provider.GetOverridePath(), expectedPath)
|
t.Errorf("GetOverridePath() = %q, want %q", provider.GetOverridePath(), expectedPath)
|
||||||
}
|
}
|
||||||
@@ -270,6 +273,104 @@ windowrulev2 = tile, class:^(extraapp)$
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseHyprlandLuaRequiresFragment(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mainLua := filepath.Join(tmpDir, "hyprland.lua")
|
||||||
|
fragLua := filepath.Join(dmsDir, "windowrules.lua")
|
||||||
|
|
||||||
|
if err := os.WriteFile(fragLua, []byte(`
|
||||||
|
hl.window_rule({ match = { class = "^test$" }, float = true })
|
||||||
|
`), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(mainLua, []byte(`
|
||||||
|
require("dms.windowrules")
|
||||||
|
`), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := ParseHyprlandWindowRules(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseHyprlandWindowRules: %v", err)
|
||||||
|
}
|
||||||
|
if len(res.Rules) != 1 {
|
||||||
|
t.Fatalf("expected 1 rule, got %d", len(res.Rules))
|
||||||
|
}
|
||||||
|
if !res.DMSRulesIncluded {
|
||||||
|
t.Fatal("expected dms.windowrules fragment to be marked included")
|
||||||
|
}
|
||||||
|
wr := ConvertHyprlandRulesToWindowRules(res.Rules)[0]
|
||||||
|
if wr.MatchCriteria.AppID != "^test$" || wr.Actions.OpenFloating == nil || !*wr.Actions.OpenFloating {
|
||||||
|
t.Fatalf("unexpected merged rule: %#v", wr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHyprlandLuaNoInitialFocusAlias(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.lua"), []byte(`
|
||||||
|
hl.window_rule({
|
||||||
|
match = { class = "^steam$" },
|
||||||
|
no_initial_focus = true,
|
||||||
|
})
|
||||||
|
`), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := ParseHyprlandWindowRules(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseHyprlandWindowRules: %v", err)
|
||||||
|
}
|
||||||
|
if len(res.Rules) != 1 {
|
||||||
|
t.Fatalf("expected 1 rule, got %d", len(res.Rules))
|
||||||
|
}
|
||||||
|
wr := ConvertHyprlandRulesToWindowRules(res.Rules)[0]
|
||||||
|
if wr.Actions.NoFocus == nil || !*wr.Actions.NoFocus {
|
||||||
|
t.Fatalf("expected no_initial_focus to populate NoFocus action: %#v", wr.Actions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatLuaManagedHyprRuleUsesLuaFieldNames(t *testing.T) {
|
||||||
|
enabled := true
|
||||||
|
rule := windowrules.WindowRule{
|
||||||
|
ID: "test-rule",
|
||||||
|
Enabled: true,
|
||||||
|
MatchCriteria: windowrules.MatchCriteria{
|
||||||
|
AppID: "^app$",
|
||||||
|
},
|
||||||
|
Actions: windowrules.Actions{
|
||||||
|
NoFocus: &enabled,
|
||||||
|
NoShadow: &enabled,
|
||||||
|
NoDim: &enabled,
|
||||||
|
NoBlur: &enabled,
|
||||||
|
NoAnim: &enabled,
|
||||||
|
ForcergbX: &enabled,
|
||||||
|
Idleinhibit: "focus",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := formatLuaManagedHyprRule(rule)
|
||||||
|
joined := strings.Join(lines, "\n")
|
||||||
|
for _, want := range []string{
|
||||||
|
"no_focus = true",
|
||||||
|
"no_shadow = true",
|
||||||
|
"no_dim = true",
|
||||||
|
"no_blur = true",
|
||||||
|
"no_anim = true",
|
||||||
|
"force_rgbx = true",
|
||||||
|
`idle_inhibit = "focus"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(joined, want) {
|
||||||
|
t.Fatalf("formatted rule missing %q: %s", want, joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBoolToInt(t *testing.T) {
|
func TestBoolToInt(t *testing.T) {
|
||||||
if boolToInt(true) != 1 {
|
if boolToInt(true) != 1 {
|
||||||
t.Error("boolToInt(true) should be 1")
|
t.Error("boolToInt(true) should be 1")
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# Hyprland Lua Migration
|
||||||
|
|
||||||
|
Hyprland 0.55 moved configuration toward Lua. DMS now follows that path for new
|
||||||
|
Hyprland setup and migration.
|
||||||
|
|
||||||
|
This guide covers what changes, where files live, and how to check that your
|
||||||
|
session is using the new config.
|
||||||
|
|
||||||
|
## Quick Summary
|
||||||
|
|
||||||
|
DMS now deploys Hyprland as:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/hypr/hyprland.lua
|
||||||
|
~/.config/hypr/dms/*.lua
|
||||||
|
```
|
||||||
|
|
||||||
|
The old hyprlang files are moved out of the active config tree:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/hypr/hyprland.conf
|
||||||
|
~/.config/hypr/dms/*.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Backups are stored here:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/hypr/.dms-backups/<timestamp>/
|
||||||
|
```
|
||||||
|
|
||||||
|
## What `dms setup` Does
|
||||||
|
|
||||||
|
When Hyprland is selected, `dms setup` writes a Lua main config and DMS Lua
|
||||||
|
fragments.
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `hyprland.lua` | Main Hyprland config. |
|
||||||
|
| `dms/colors.lua` | Theme colors. |
|
||||||
|
| `dms/outputs.lua` | Monitors and display settings. |
|
||||||
|
| `dms/layout.lua` | Layout, gaps, borders, and decoration. |
|
||||||
|
| `dms/cursor.lua` | Cursor settings. |
|
||||||
|
| `dms/binds.lua` | DMS-managed default shortcuts. |
|
||||||
|
| `dms/binds-user.lua` | User shortcut overrides. |
|
||||||
|
| `dms/windowrules.lua` | Window rules. |
|
||||||
|
|
||||||
|
`dms/binds.lua` is managed by DMS and may be refreshed by setup. Put custom
|
||||||
|
keyboard shortcuts in `dms/binds-user.lua`, or use the Keyboard Shortcuts page in
|
||||||
|
DMS Settings.
|
||||||
|
|
||||||
|
Most other existing non-empty Lua fragments are preserved.
|
||||||
|
|
||||||
|
## Legacy Config Migration
|
||||||
|
|
||||||
|
During migration, DMS moves legacy active files into the backup folder so
|
||||||
|
Hyprland does not see both config formats at once.
|
||||||
|
|
||||||
|
DMS also migrates legacy `monitor = ...` lines from `hyprland.conf` into
|
||||||
|
`dms/outputs.lua` when `outputs.lua` is empty or missing. If you already have a
|
||||||
|
custom `outputs.lua`, DMS leaves it alone.
|
||||||
|
|
||||||
|
## DMS Settings Support
|
||||||
|
|
||||||
|
DMS Settings now targets Lua files for Hyprland:
|
||||||
|
|
||||||
|
| Settings page | Lua file |
|
||||||
|
| --- | --- |
|
||||||
|
| Keyboard Shortcuts | `dms/binds-user.lua` |
|
||||||
|
| Displays | `dms/outputs.lua` |
|
||||||
|
| Theme Colors | `dms/colors.lua` |
|
||||||
|
| Cursor | `dms/cursor.lua` |
|
||||||
|
| Window Rules | `dms/windowrules.lua` |
|
||||||
|
|
||||||
|
The main config should include the DMS fragments:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
require("dms.colors")
|
||||||
|
require("dms.outputs")
|
||||||
|
require("dms.layout")
|
||||||
|
require("dms.cursor")
|
||||||
|
require("dms.binds")
|
||||||
|
require("dms.binds-user")
|
||||||
|
require("dms.windowrules")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keyboard Shortcuts: Delete and Reset
|
||||||
|
|
||||||
|
The Keyboard Shortcuts page exposes two actions on any DMS-managed bind:
|
||||||
|
|
||||||
|
- **Delete** — removes the shortcut entirely. For default DMS shortcuts (from
|
||||||
|
`dms/binds.lua`), this saves an `hl.unbind("KEY")` line into
|
||||||
|
`dms/binds-user.lua` so the removal sticks across `dms setup` runs.
|
||||||
|
- **Reset to default** — only visible when you are editing a user override of
|
||||||
|
a DMS default. It drops your override so the original DMS default re-applies.
|
||||||
|
|
||||||
|
Binds from your own `hyprland.lua` (outside the `dms/` folder) are read-only
|
||||||
|
in Settings — DMS does not write into files it does not manage.
|
||||||
|
|
||||||
|
## Starting Hyprland
|
||||||
|
|
||||||
|
For the Lua config to be active, Hyprland must start with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
Hyprland -c ~/.config/hypr/hyprland.lua
|
||||||
|
```
|
||||||
|
|
||||||
|
If Hyprland warns that it is using an autogenerated config, or the warning
|
||||||
|
mentions `hyprland.conf`, the session is not using the DMS Lua config yet.
|
||||||
|
|
||||||
|
## Verify Everything
|
||||||
|
|
||||||
|
After updating DMS, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dms setup
|
||||||
|
hyprctl reload
|
||||||
|
hyprctl configerrors
|
||||||
|
```
|
||||||
|
|
||||||
|
If the current session was not started from `hyprland.lua`, restart Hyprland with
|
||||||
|
the Lua config and check again.
|
||||||
|
|
||||||
|
Useful file checks:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
test -f ~/.config/hypr/hyprland.lua
|
||||||
|
test ! -f ~/.config/hypr/hyprland.conf
|
||||||
|
ls ~/.config/hypr/dms
|
||||||
|
```
|
||||||
|
|
||||||
|
The live `dms` folder should contain Lua files like `binds.lua`,
|
||||||
|
`binds-user.lua`, `outputs.lua`, and `windowrules.lua`.
|
||||||
|
|
||||||
|
Note: Hyprland 0.55 still auto-generates `hyprland.conf` if you launch it
|
||||||
|
without `-c ~/.config/hypr/hyprland.lua`. DMS sweeps any stray
|
||||||
|
`hyprland.conf` into `.dms-backups/<timestamp>/` on the next `dms run`
|
||||||
|
startup, so the second check above is the right long-term state. If you see
|
||||||
|
`hyprland.conf` persist between `dms run` invocations, the session was not
|
||||||
|
started from `hyprland.lua` — restart Hyprland with the `-c` flag (or update
|
||||||
|
your session/desktop entry to include it).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If shortcuts do not work, confirm `hyprland.lua` includes both:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
require("dms.binds")
|
||||||
|
require("dms.binds-user")
|
||||||
|
```
|
||||||
|
|
||||||
|
If `hyprctl configerrors` reports errors in `dms/binds.lua`, rerun `dms setup`
|
||||||
|
with the latest DMS binary so the DMS-managed shortcut file is refreshed.
|
||||||
|
|
||||||
|
If a migrated monitor setup looks wrong, compare:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/hypr/dms/outputs.lua
|
||||||
|
~/.config/hypr/.dms-backups/<timestamp>/
|
||||||
|
```
|
||||||
|
|
||||||
|
Your previous config should be available in the timestamped backup folder.
|
||||||
|
|
||||||
|
## Reference Map
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/hypr/
|
||||||
|
|-- hyprland.lua # Main DMS Hyprland config
|
||||||
|
|-- .dms-backups/ # Timestamped backups from setup/migration
|
||||||
|
`-- dms/
|
||||||
|
|-- colors.lua # Theme colors
|
||||||
|
|-- outputs.lua # Monitor/output config
|
||||||
|
|-- layout.lua # Layout, gaps, borders, decoration
|
||||||
|
|-- cursor.lua # Cursor settings
|
||||||
|
|-- binds.lua # DMS-managed default shortcuts
|
||||||
|
|-- binds-user.lua # User shortcut overrides
|
||||||
|
`-- windowrules.lua # DMS-managed window rules
|
||||||
|
```
|
||||||
|
|
||||||
|
Legacy files such as `hyprland.conf` and `dms/*.conf` should live in
|
||||||
|
`.dms-backups/<timestamp>/` after migration, not in the active config tree.
|
||||||
|
|
||||||
|
## Maintainer Note
|
||||||
|
|
||||||
|
Embedded source files live in `core/internal/config/embedded/` and use names like
|
||||||
|
`hypr-binds.lua`. Installed user files use shorter names like `dms/binds.lua`.
|
||||||
|
|
||||||
|
After changing Hyprland config deployment or parsing, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd core
|
||||||
|
go test ./internal/config ./internal/keybinds/providers ./internal/windowrules/providers
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
function shQuote(value) {
|
||||||
|
return "'" + String(value ?? "").replace(/'/g, "'\\''") + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
function dirname(path) {
|
||||||
|
const idx = String(path ?? "").lastIndexOf("/");
|
||||||
|
return idx > 0 ? path.substring(0, idx) : ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRepairScript(options) {
|
||||||
|
const configFile = options.configFile;
|
||||||
|
const backupFile = options.backupFile;
|
||||||
|
const fragments = options.fragmentFiles || (options.fragmentFile ? [options.fragmentFile] : []);
|
||||||
|
const includes = options.includes || [{
|
||||||
|
grepPattern: options.grepPattern,
|
||||||
|
includeLine: options.includeLine
|
||||||
|
}];
|
||||||
|
|
||||||
|
const commands = [];
|
||||||
|
if (backupFile)
|
||||||
|
commands.push(`cp ${shQuote(configFile)} ${shQuote(backupFile)} 2>/dev/null || true`);
|
||||||
|
|
||||||
|
const dirs = {};
|
||||||
|
for (const fragment of fragments)
|
||||||
|
dirs[dirname(fragment)] = true;
|
||||||
|
for (const dir in dirs)
|
||||||
|
commands.push(`mkdir -p ${shQuote(dir)}`);
|
||||||
|
if (fragments.length > 0)
|
||||||
|
commands.push("touch " + fragments.map(shQuote).join(" "));
|
||||||
|
|
||||||
|
for (const include of includes) {
|
||||||
|
if (!include.grepPattern || !include.includeLine)
|
||||||
|
continue;
|
||||||
|
commands.push(`if ! grep -v '^[[:space:]]*\\(//\\|#\\|--\\)' ${shQuote(configFile)} 2>/dev/null | grep -q ${shQuote(include.grepPattern)}; then echo '' >> ${shQuote(configFile)} && printf '%s\\n' ${shQuote(include.includeLine)} >> ${shQuote(configFile)}; fi`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands.join("; ");
|
||||||
|
}
|
||||||
@@ -178,7 +178,7 @@ sudo systemctl enable greetd
|
|||||||
#### Legacy installation (deprecated)
|
#### Legacy installation (deprecated)
|
||||||
|
|
||||||
If you prefer the old method with separate shell scripts and config files:
|
If you prefer the old method with separate shell scripts and config files:
|
||||||
1. Copy `assets/dms-niri.kdl` or `assets/dms-hypr.conf` to `/etc/greetd`
|
1. Copy `assets/dms-niri.kdl` or `assets/dms-hypr.lua` (legacy: `assets/dms-hypr.conf`) to `/etc/greetd`
|
||||||
2. Copy `assets/greet-niri.sh` or `assets/greet-hyprland.sh` to `/usr/local/bin/start-dms-greetd.sh`
|
2. Copy `assets/greet-niri.sh` or `assets/greet-hyprland.sh` to `/usr/local/bin/start-dms-greetd.sh`
|
||||||
3. Edit the config file and replace `_DMS_PATH_` with your DMS installation path
|
3. Edit the config file and replace `_DMS_PATH_` with your DMS installation path
|
||||||
4. Configure greetd to use `/usr/local/bin/start-dms-greetd.sh`
|
4. Configure greetd to use `/usr/local/bin/start-dms-greetd.sh`
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# Deprecated: greetd expects Hyprland 0.55+ Lua; use `/etc/greetd/dms-hypr.lua` instead.
|
||||||
env = DMS_RUN_GREETER,1
|
env = DMS_RUN_GREETER,1
|
||||||
|
|
||||||
exec = sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"
|
exec = sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- Minimal Hyprland (Lua) session for greetd — replace _DMS_PATH_ with your DMS checkout.
|
||||||
|
-- Copy to `/etc/greetd/dms-hypr.lua` alongside `greet-hyprland.sh`.
|
||||||
|
|
||||||
|
hl.env("DMS_RUN_GREETER", "1")
|
||||||
|
|
||||||
|
hl.on("hyprland.start", function()
|
||||||
|
hl.exec_cmd('sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"')
|
||||||
|
end)
|
||||||
@@ -5,7 +5,7 @@ export QT_QPA_PLATFORM=wayland
|
|||||||
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
||||||
export EGL_PLATFORM=gbm
|
export EGL_PLATFORM=gbm
|
||||||
if command -v start-hyprland >/dev/null 2>&1; then
|
if command -v start-hyprland >/dev/null 2>&1; then
|
||||||
exec start-hyprland -- -c /etc/greetd/dms-hypr.conf
|
exec start-hyprland -- -c /etc/greetd/dms-hypr.lua
|
||||||
else
|
else
|
||||||
exec Hyprland -c /etc/greetd/dms-hypr.conf
|
exec Hyprland -c /etc/greetd/dms-hypr.lua
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Quickshell
|
|||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
import "../../../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
@@ -1074,10 +1075,86 @@ Singleton {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hyprLuaField(line, field) {
|
||||||
|
const re = new RegExp("\\b" + field + "\\s*=\\s*(\\\"(?:\\\\\\\\.|[^\\\"])*\\\"|'(?:\\\\\\\\.|[^'])*'|\\[\\[.*?\\]\\]|[^,}\\s]+)");
|
||||||
|
const match = line.match(re);
|
||||||
|
if (!match)
|
||||||
|
return undefined;
|
||||||
|
const raw = match[1].trim();
|
||||||
|
if (raw.startsWith("[[") && raw.endsWith("]]"))
|
||||||
|
return raw.slice(2, -2);
|
||||||
|
if (raw.startsWith("\"")) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (e) {
|
||||||
|
return raw.slice(1, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (raw.startsWith("'") && raw.endsWith("'"))
|
||||||
|
return raw.slice(1, -1).replace(/\\'/g, "'");
|
||||||
|
if (raw === "true")
|
||||||
|
return true;
|
||||||
|
if (raw === "false")
|
||||||
|
return false;
|
||||||
|
const num = Number(raw);
|
||||||
|
return isNaN(num) ? raw : num;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHyprlandLuaMonitorLine(line) {
|
||||||
|
if (!line.match(/^\s*hl\.monitor\s*\(/))
|
||||||
|
return null;
|
||||||
|
const name = hyprLuaField(line, "output");
|
||||||
|
if (name === undefined)
|
||||||
|
return null;
|
||||||
|
const disabled = hyprLuaField(line, "disabled") === true;
|
||||||
|
const mode = hyprLuaField(line, "mode") || "preferred";
|
||||||
|
const position = hyprLuaField(line, "position") || "0x0";
|
||||||
|
const scaleValue = hyprLuaField(line, "scale");
|
||||||
|
const transform = Number(hyprLuaField(line, "transform") ?? 0);
|
||||||
|
const vrrMode = Number(hyprLuaField(line, "vrr") ?? 0);
|
||||||
|
const posMatch = String(position).match(/^(-?\d+)x(-?\d+)$/);
|
||||||
|
const modeMatch = String(mode).match(/^(\d+)x(\d+)@([\d.]+)/);
|
||||||
|
const settings = {
|
||||||
|
"disabled": disabled || undefined,
|
||||||
|
"bitdepth": hyprLuaField(line, "bitdepth"),
|
||||||
|
"colorManagement": hyprLuaField(line, "cm"),
|
||||||
|
"sdrBrightness": hyprLuaField(line, "sdrbrightness"),
|
||||||
|
"sdrSaturation": hyprLuaField(line, "sdrsaturation"),
|
||||||
|
"supportsWideColor": hyprLuaField(line, "supports_wide_color"),
|
||||||
|
"supportsHdr": hyprLuaField(line, "supports_hdr"),
|
||||||
|
"vrrFullscreenOnly": vrrMode === 2 ? true : undefined
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"name": String(name),
|
||||||
|
"logical": {
|
||||||
|
"x": posMatch ? parseInt(posMatch[1]) : 0,
|
||||||
|
"y": posMatch ? parseInt(posMatch[2]) : 0,
|
||||||
|
"scale": typeof scaleValue === "number" ? scaleValue : 1.0,
|
||||||
|
"transform": hyprlandToTransform(transform)
|
||||||
|
},
|
||||||
|
"modes": modeMatch ? [{
|
||||||
|
"width": parseInt(modeMatch[1]),
|
||||||
|
"height": parseInt(modeMatch[2]),
|
||||||
|
"refresh_rate": Math.round(parseFloat(modeMatch[3]) * 1000)
|
||||||
|
}] : [],
|
||||||
|
"current_mode": modeMatch ? 0 : -1,
|
||||||
|
"vrr_enabled": vrrMode >= 1,
|
||||||
|
"vrr_supported": vrrMode > 0,
|
||||||
|
"hyprlandSettings": settings,
|
||||||
|
"mirror": hyprLuaField(line, "mirror") || ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function parseHyprlandOutputs(content) {
|
function parseHyprlandOutputs(content) {
|
||||||
const result = {};
|
const result = {};
|
||||||
const lines = content.split("\n");
|
const lines = content.split("\n");
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
|
const luaMonitor = parseHyprlandLuaMonitorLine(line);
|
||||||
|
if (luaMonitor) {
|
||||||
|
result[luaMonitor.name] = luaMonitor;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const disableMatch = line.match(/^\s*monitor\s*=\s*([^,]+),\s*disable\s*$/);
|
const disableMatch = line.match(/^\s*monitor\s*=\s*([^,]+),\s*disable\s*$/);
|
||||||
if (disableMatch) {
|
if (disableMatch) {
|
||||||
const name = disableMatch[1].trim();
|
const name = disableMatch[1].trim();
|
||||||
@@ -1269,10 +1346,10 @@ Singleton {
|
|||||||
};
|
};
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
return {
|
return {
|
||||||
"configFile": configDir + "/hypr/hyprland.conf",
|
"configFile": configDir + "/hypr/hyprland.lua",
|
||||||
"outputsFile": configDir + "/hypr/dms/outputs.conf",
|
"outputsFile": configDir + "/hypr/dms/outputs.lua",
|
||||||
"grepPattern": 'source.*dms/outputs.conf',
|
"grepPattern": "dms.outputs",
|
||||||
"includeLine": "source = ./dms/outputs.conf"
|
"includeLine": "require(\"dms.outputs\")"
|
||||||
};
|
};
|
||||||
case "dwl":
|
case "dwl":
|
||||||
return {
|
return {
|
||||||
@@ -1296,7 +1373,7 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filename = (compositor === "niri") ? "outputs.kdl" : "outputs.conf";
|
const filename = (compositor === "niri") ? "outputs.kdl" : ((compositor === "hyprland") ? "outputs.lua" : "outputs.conf");
|
||||||
const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
|
const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
|
||||||
|
|
||||||
checkingInclude = true;
|
checkingInclude = true;
|
||||||
@@ -1326,11 +1403,17 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
fixingInclude = true;
|
fixingInclude = true;
|
||||||
const outputsDir = paths.outputsFile.substring(0, paths.outputsFile.lastIndexOf("/"));
|
|
||||||
const unixTime = Math.floor(Date.now() / 1000);
|
const unixTime = Math.floor(Date.now() / 1000);
|
||||||
const backupFile = paths.configFile + ".backup" + unixTime;
|
const backupFile = paths.configFile + ".backup" + unixTime;
|
||||||
|
const script = ConfigIncludeResolve.buildRepairScript({
|
||||||
|
configFile: paths.configFile,
|
||||||
|
backupFile: backupFile,
|
||||||
|
fragmentFile: paths.outputsFile,
|
||||||
|
grepPattern: paths.grepPattern,
|
||||||
|
includeLine: paths.includeLine
|
||||||
|
});
|
||||||
|
|
||||||
Proc.runCommand("fix-outputs-include", ["sh", "-c", `cp "${paths.configFile}" "${backupFile}" 2>/dev/null; ` + `mkdir -p "${outputsDir}" && ` + `touch "${paths.outputsFile}" && ` + `if ! grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" 2>/dev/null | grep -q '${paths.grepPattern}'; then ` + `echo '' >> "${paths.configFile}" && ` + `echo '${paths.includeLine}' >> "${paths.configFile}"; fi`], (output, exitCode) => {
|
Proc.runCommand("fix-outputs-include", ["sh", "-c", script], (output, exitCode) => {
|
||||||
fixingInclude = false;
|
fixingInclude = false;
|
||||||
if (exitCode !== 0)
|
if (exitCode !== 0)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
@@ -96,6 +97,32 @@ Item {
|
|||||||
expandedKey = bindData.action;
|
expandedKey = bindData.action;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function confirmRemoveBind(key, remainingKey) {
|
||||||
|
removeBindConfirm.showWithOptions({
|
||||||
|
title: I18n.tr("Remove Shortcut?"),
|
||||||
|
message: KeybindsService.currentProvider === "hyprland" ? I18n.tr("Remove the shortcut %1? An unbind entry will be saved to dms/binds-user.lua so it stays removed across DMS updates.").arg(key) : I18n.tr("Remove the shortcut %1?").arg(key),
|
||||||
|
confirmText: I18n.tr("Remove"),
|
||||||
|
confirmColor: Theme.primary,
|
||||||
|
onConfirm: () => {
|
||||||
|
KeybindsService.removeBind(key);
|
||||||
|
keybindsTab._editingKey = remainingKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmResetBind(key, remainingKey) {
|
||||||
|
removeBindConfirm.showWithOptions({
|
||||||
|
title: I18n.tr("Reset to Default?"),
|
||||||
|
message: I18n.tr("Drop your override for %1 so the DMS default action re-applies?").arg(key),
|
||||||
|
confirmText: I18n.tr("Reset"),
|
||||||
|
confirmColor: Theme.primary,
|
||||||
|
onConfirm: () => {
|
||||||
|
KeybindsService.resetBind(key);
|
||||||
|
keybindsTab._editingKey = remainingKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function _onSaveSuccess() {
|
function _onSaveSuccess() {
|
||||||
if (showingNewBind) {
|
if (showingNewBind) {
|
||||||
showingNewBind = false;
|
showingNewBind = false;
|
||||||
@@ -129,6 +156,10 @@ Item {
|
|||||||
onTriggered: keybindsTab._updateFiltered()
|
onTriggered: keybindsTab._updateFiltered()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfirmModal {
|
||||||
|
id: removeBindConfirm
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: KeybindsService
|
target: KeybindsService
|
||||||
function onBindsLoaded() {
|
function onBindsLoaded() {
|
||||||
@@ -238,7 +269,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : "dms/binds.conf"
|
readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : KeybindsService.currentProvider === "hyprland" ? "dms/binds-user.lua" : "dms/binds.conf"
|
||||||
text: I18n.tr("Click any shortcut to edit. Changes save to %1").arg(bindsFile)
|
text: I18n.tr("Click any shortcut to edit. Changes save to %1").arg(bindsFile)
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
@@ -336,7 +367,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : "dms/binds.conf"
|
readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : KeybindsService.currentProvider === "hyprland" ? "dms/binds-user.lua" : "dms/binds.conf"
|
||||||
text: {
|
text: {
|
||||||
if (warningBox.showSetup)
|
if (warningBox.showSetup)
|
||||||
return I18n.tr("Click 'Setup' to create %1 and add include to config.").arg(bindsFile);
|
return I18n.tr("Click 'Setup' to create %1 and add include to config.").arg(bindsFile);
|
||||||
@@ -623,8 +654,11 @@ Item {
|
|||||||
}
|
}
|
||||||
onRemoveBind: key => {
|
onRemoveBind: key => {
|
||||||
const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? "";
|
const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? "";
|
||||||
KeybindsService.removeBind(key);
|
keybindsTab.confirmRemoveBind(key, remainingKey);
|
||||||
keybindsTab._editingKey = remainingKey;
|
}
|
||||||
|
onResetBind: key => {
|
||||||
|
const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? "";
|
||||||
|
keybindsTab.confirmResetBind(key, remainingKey);
|
||||||
}
|
}
|
||||||
onIsExpandedChanged: {
|
onIsExpandedChanged: {
|
||||||
if (!isExpanded || !keybindsTab._editingKey)
|
if (!isExpanded || !keybindsTab._editingKey)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import qs.Modals.FileBrowser
|
|||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import qs.Modules.Settings.Widgets
|
import qs.Modules.Settings.Widgets
|
||||||
|
import "../../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: themeColorsTab
|
id: themeColorsTab
|
||||||
@@ -39,10 +40,10 @@ Item {
|
|||||||
};
|
};
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
return {
|
return {
|
||||||
"configFile": configDir + "/hypr/hyprland.conf",
|
"configFile": configDir + "/hypr/hyprland.lua",
|
||||||
"cursorFile": configDir + "/hypr/dms/cursor.conf",
|
"cursorFile": configDir + "/hypr/dms/cursor.lua",
|
||||||
"grepPattern": 'source.*dms/cursor.conf',
|
"grepPattern": "dms.cursor",
|
||||||
"includeLine": "source = ./dms/cursor.conf"
|
"includeLine": "require(\"dms.cursor\")"
|
||||||
};
|
};
|
||||||
case "dwl":
|
case "dwl":
|
||||||
return {
|
return {
|
||||||
@@ -66,7 +67,7 @@ Item {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filename = (compositor === "niri") ? "cursor.kdl" : "cursor.conf";
|
const filename = (compositor === "niri") ? "cursor.kdl" : ((compositor === "hyprland") ? "cursor.lua" : "cursor.conf");
|
||||||
const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
|
const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
|
||||||
|
|
||||||
checkingCursorInclude = true;
|
checkingCursorInclude = true;
|
||||||
@@ -95,10 +96,16 @@ Item {
|
|||||||
if (!paths)
|
if (!paths)
|
||||||
return;
|
return;
|
||||||
fixingCursorInclude = true;
|
fixingCursorInclude = true;
|
||||||
const cursorDir = paths.cursorFile.substring(0, paths.cursorFile.lastIndexOf("/"));
|
|
||||||
const unixTime = Math.floor(Date.now() / 1000);
|
const unixTime = Math.floor(Date.now() / 1000);
|
||||||
const backupFile = paths.configFile + ".backup" + unixTime;
|
const backupFile = paths.configFile + ".backup" + unixTime;
|
||||||
Proc.runCommand("fix-cursor-include", ["sh", "-c", `cp "${paths.configFile}" "${backupFile}" 2>/dev/null; ` + `mkdir -p "${cursorDir}" && ` + `touch "${paths.cursorFile}" && ` + `if ! grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" 2>/dev/null | grep -q '${paths.grepPattern}'; then ` + `echo '' >> "${paths.configFile}" && ` + `echo '${paths.includeLine}' >> "${paths.configFile}"; fi`], (output, exitCode) => {
|
const script = ConfigIncludeResolve.buildRepairScript({
|
||||||
|
configFile: paths.configFile,
|
||||||
|
backupFile: backupFile,
|
||||||
|
fragmentFile: paths.cursorFile,
|
||||||
|
grepPattern: paths.grepPattern,
|
||||||
|
includeLine: paths.includeLine
|
||||||
|
});
|
||||||
|
Proc.runCommand("fix-cursor-include", ["sh", "-c", script], (output, exitCode) => {
|
||||||
fixingCursorInclude = false;
|
fixingCursorInclude = false;
|
||||||
if (exitCode !== 0)
|
if (exitCode !== 0)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Quickshell.Wayland
|
|||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
import "../../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -54,10 +55,10 @@ Item {
|
|||||||
};
|
};
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
return {
|
return {
|
||||||
"configFile": configDir + "/hypr/hyprland.conf",
|
"configFile": configDir + "/hypr/hyprland.lua",
|
||||||
"rulesFile": configDir + "/hypr/dms/windowrules.conf",
|
"rulesFile": configDir + "/hypr/dms/windowrules.lua",
|
||||||
"grepPattern": 'source.*dms/windowrules.conf',
|
"grepPattern": "dms.windowrules",
|
||||||
"includeLine": "source = ./dms/windowrules.conf"
|
"includeLine": "require(\"dms.windowrules\")"
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
@@ -135,7 +136,7 @@ Item {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filename = (compositor === "niri") ? "windowrules.kdl" : "windowrules.conf";
|
const filename = (compositor === "niri") ? "windowrules.kdl" : "windowrules.lua";
|
||||||
checkingInclude = true;
|
checkingInclude = true;
|
||||||
Proc.runCommand("check-windowrules-include", ["dms", "config", "resolve-include", compositor, filename], (output, exitCode) => {
|
Proc.runCommand("check-windowrules-include", ["dms", "config", "resolve-include", compositor, filename], (output, exitCode) => {
|
||||||
checkingInclude = false;
|
checkingInclude = false;
|
||||||
@@ -162,10 +163,16 @@ Item {
|
|||||||
if (!paths)
|
if (!paths)
|
||||||
return;
|
return;
|
||||||
fixingInclude = true;
|
fixingInclude = true;
|
||||||
const rulesDir = paths.rulesFile.substring(0, paths.rulesFile.lastIndexOf("/"));
|
|
||||||
const unixTime = Math.floor(Date.now() / 1000);
|
const unixTime = Math.floor(Date.now() / 1000);
|
||||||
const backupFile = paths.configFile + ".backup" + unixTime;
|
const backupFile = paths.configFile + ".backup" + unixTime;
|
||||||
Proc.runCommand("fix-windowrules-include", ["sh", "-c", `cp "${paths.configFile}" "${backupFile}" 2>/dev/null; ` + `mkdir -p "${rulesDir}" && ` + `touch "${paths.rulesFile}" && ` + `if ! grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" 2>/dev/null | grep -q '${paths.grepPattern}'; then ` + `echo '' >> "${paths.configFile}" && ` + `echo '${paths.includeLine}' >> "${paths.configFile}"; fi`], (output, exitCode) => {
|
const script = ConfigIncludeResolve.buildRepairScript({
|
||||||
|
configFile: paths.configFile,
|
||||||
|
backupFile: backupFile,
|
||||||
|
fragmentFile: paths.rulesFile,
|
||||||
|
grepPattern: paths.grepPattern,
|
||||||
|
includeLine: paths.includeLine
|
||||||
|
});
|
||||||
|
Proc.runCommand("fix-windowrules-include", ["sh", "-c", script], (output, exitCode) => {
|
||||||
fixingInclude = false;
|
fixingInclude = false;
|
||||||
if (exitCode !== 0)
|
if (exitCode !== 0)
|
||||||
return;
|
return;
|
||||||
@@ -252,7 +259,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Define rules for window behavior. Saves to %1").arg(CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.conf")
|
text: I18n.tr("Define rules for window behavior. Saves to %1").arg(CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.lua")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
@@ -351,7 +358,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
readonly property string rulesFile: CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.conf"
|
readonly property string rulesFile: CompositorService.isNiri ? "dms/windowrules.kdl" : "dms/windowrules.lua"
|
||||||
text: warningBox.showSetup ? I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.").arg(rulesFile) : I18n.tr("%1 exists but is not included. Window rules won't apply.").arg(rulesFile)
|
text: warningBox.showSetup ? I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.").arg(rulesFile) : I18n.tr("%1 exists but is not included. Window rules won't apply.").arg(rulesFile)
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ Singleton {
|
|||||||
|
|
||||||
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
||||||
readonly property string hyprDmsDir: configDir + "/hypr/dms"
|
readonly property string hyprDmsDir: configDir + "/hypr/dms"
|
||||||
readonly property string outputsPath: hyprDmsDir + "/outputs.conf"
|
readonly property string outputsPath: hyprDmsDir + "/outputs.lua"
|
||||||
readonly property string layoutPath: hyprDmsDir + "/layout.conf"
|
readonly property string layoutPath: hyprDmsDir + "/layout.lua"
|
||||||
readonly property string cursorPath: hyprDmsDir + "/cursor.conf"
|
readonly property string cursorPath: hyprDmsDir + "/cursor.lua"
|
||||||
readonly property string windowrulesPath: hyprDmsDir + "/windowrules.conf"
|
readonly property string windowrulesPath: hyprDmsDir + "/windowrules.lua"
|
||||||
|
|
||||||
property int _lastGapValue: -1
|
property int _lastGapValue: -1
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ Singleton {
|
|||||||
function ensureWindowrulesConfig() {
|
function ensureWindowrulesConfig() {
|
||||||
Proc.runCommand("hypr-ensure-windowrules", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && [ ! -f "${windowrulesPath}" ] && touch "${windowrulesPath}" || true`], (output, exitCode) => {
|
Proc.runCommand("hypr-ensure-windowrules", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && [ ! -f "${windowrulesPath}" ] && touch "${windowrulesPath}" || true`], (output, exitCode) => {
|
||||||
if (exitCode !== 0)
|
if (exitCode !== 0)
|
||||||
log.warn("Failed to ensure windowrules.conf:", output);
|
log.warn("Failed to ensure windowrules.lua:", output);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +62,18 @@ Singleton {
|
|||||||
return outputName;
|
return outputName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function luaQuoted(str) {
|
||||||
|
return JSON.stringify(String(str ?? ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceFlagValue(value) {
|
||||||
|
if (value === true)
|
||||||
|
return 1;
|
||||||
|
if (value === false)
|
||||||
|
return -1;
|
||||||
|
return Number(value);
|
||||||
|
}
|
||||||
|
|
||||||
function generateOutputsConfig(outputsData, hyprlandSettings, callback) {
|
function generateOutputsConfig(outputsData, hyprlandSettings, callback) {
|
||||||
if (!outputsData || Object.keys(outputsData).length === 0) {
|
if (!outputsData || Object.keys(outputsData).length === 0) {
|
||||||
if (callback)
|
if (callback)
|
||||||
@@ -70,8 +82,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const settings = hyprlandSettings || SettingsData.hyprlandOutputSettings;
|
const settings = hyprlandSettings || SettingsData.hyprlandOutputSettings;
|
||||||
let lines = ["# Auto-generated by DMS - do not edit manually", ""];
|
let lines = ["-- Auto-generated by DMS — do not edit manually", ""];
|
||||||
let monitorv2Blocks = [];
|
|
||||||
|
|
||||||
for (const outputName in outputsData) {
|
for (const outputName in outputsData) {
|
||||||
const output = outputsData[outputName];
|
const output = outputsData[outputName];
|
||||||
@@ -82,7 +93,7 @@ Singleton {
|
|||||||
const outputSettings = settings[identifier] || {};
|
const outputSettings = settings[identifier] || {};
|
||||||
|
|
||||||
if (outputSettings.disabled) {
|
if (outputSettings.disabled) {
|
||||||
lines.push("monitor = " + identifier + ", disable");
|
lines.push(`hl.monitor({ output = ${luaQuoted(identifier)}, disabled = true })`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,68 +109,42 @@ Singleton {
|
|||||||
const position = x + "x" + y;
|
const position = x + "x" + y;
|
||||||
const scale = output.logical?.scale ?? 1.0;
|
const scale = output.logical?.scale ?? 1.0;
|
||||||
|
|
||||||
let monitorLine = "monitor = " + identifier + ", " + resolution + ", " + position + ", " + scale;
|
const parts = [`output = ${luaQuoted(identifier)}`, `mode = ${luaQuoted(resolution)}`, `position = ${luaQuoted(position)}`, `scale = ${Number(scale)}`];
|
||||||
|
|
||||||
const transform = transformToHyprland(output.logical?.transform ?? "Normal");
|
const transform = transformToHyprland(output.logical?.transform ?? "Normal");
|
||||||
if (transform !== 0)
|
if (transform !== 0)
|
||||||
monitorLine += ", transform, " + transform;
|
parts.push(`transform = ${transform}`);
|
||||||
|
|
||||||
if (output.vrr_supported) {
|
if (output.vrr_supported) {
|
||||||
const vrrMode = outputSettings.vrrFullscreenOnly ? 2 : (output.vrr_enabled ? 1 : 0);
|
const vrrMode = outputSettings.vrrFullscreenOnly ? 2 : (output.vrr_enabled ? 1 : 0);
|
||||||
monitorLine += ", vrr, " + vrrMode;
|
parts.push(`vrr = ${vrrMode}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (output.mirror && output.mirror.length > 0)
|
if (output.mirror && output.mirror.length > 0)
|
||||||
monitorLine += ", mirror, " + output.mirror;
|
parts.push(`mirror = ${luaQuoted(output.mirror)}`);
|
||||||
|
|
||||||
if (outputSettings.bitdepth && outputSettings.bitdepth !== 8)
|
if (outputSettings.bitdepth && outputSettings.bitdepth !== 8)
|
||||||
monitorLine += ", bitdepth, " + outputSettings.bitdepth;
|
parts.push(`bitdepth = ${Number(outputSettings.bitdepth)}`);
|
||||||
|
|
||||||
if (outputSettings.colorManagement && outputSettings.colorManagement !== "auto")
|
if (outputSettings.colorManagement && outputSettings.colorManagement !== "auto")
|
||||||
monitorLine += ", cm, " + outputSettings.colorManagement;
|
parts.push(`cm = ${luaQuoted(outputSettings.colorManagement)}`);
|
||||||
|
|
||||||
if (outputSettings.sdrBrightness !== undefined && outputSettings.sdrBrightness !== 1.0)
|
if (outputSettings.sdrBrightness !== undefined && outputSettings.sdrBrightness !== 1.0)
|
||||||
monitorLine += ", sdrbrightness, " + outputSettings.sdrBrightness;
|
parts.push(`sdrbrightness = ${Number(outputSettings.sdrBrightness)}`);
|
||||||
|
|
||||||
if (outputSettings.sdrSaturation !== undefined && outputSettings.sdrSaturation !== 1.0)
|
if (outputSettings.sdrSaturation !== undefined && outputSettings.sdrSaturation !== 1.0)
|
||||||
monitorLine += ", sdrsaturation, " + outputSettings.sdrSaturation;
|
parts.push(`sdrsaturation = ${Number(outputSettings.sdrSaturation)}`);
|
||||||
|
|
||||||
lines.push(monitorLine);
|
if (outputSettings.supportsWideColor !== undefined)
|
||||||
|
parts.push(`supports_wide_color = ${forceFlagValue(outputSettings.supportsWideColor)}`);
|
||||||
|
|
||||||
const needsMonitorv2 = outputSettings.supportsHdr || outputSettings.supportsWideColor || outputSettings.sdrMinLuminance !== undefined || outputSettings.sdrMaxLuminance !== undefined || outputSettings.minLuminance !== undefined || outputSettings.maxLuminance !== undefined || outputSettings.maxAvgLuminance !== undefined;
|
if (outputSettings.supportsHdr !== undefined)
|
||||||
|
parts.push(`supports_hdr = ${forceFlagValue(outputSettings.supportsHdr)}`);
|
||||||
|
|
||||||
if (needsMonitorv2) {
|
lines.push("hl.monitor({ " + parts.join(", ") + " })");
|
||||||
let block = "monitorv2 {\n";
|
|
||||||
block += " output = " + identifier + "\n";
|
|
||||||
|
|
||||||
if (outputSettings.supportsWideColor)
|
|
||||||
block += " supports_wide_color = true\n";
|
|
||||||
if (outputSettings.supportsHdr)
|
|
||||||
block += " supports_hdr = true\n";
|
|
||||||
if (outputSettings.sdrMinLuminance !== undefined)
|
|
||||||
block += " sdr_min_luminance = " + outputSettings.sdrMinLuminance + "\n";
|
|
||||||
if (outputSettings.sdrMaxLuminance !== undefined)
|
|
||||||
block += " sdr_max_luminance = " + outputSettings.sdrMaxLuminance + "\n";
|
|
||||||
if (outputSettings.minLuminance !== undefined)
|
|
||||||
block += " min_luminance = " + outputSettings.minLuminance + "\n";
|
|
||||||
if (outputSettings.maxLuminance !== undefined)
|
|
||||||
block += " max_luminance = " + outputSettings.maxLuminance + "\n";
|
|
||||||
if (outputSettings.maxAvgLuminance !== undefined)
|
|
||||||
block += " max_avg_luminance = " + outputSettings.maxAvgLuminance + "\n";
|
|
||||||
|
|
||||||
block += "}";
|
|
||||||
monitorv2Blocks.push(block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (monitorv2Blocks.length > 0) {
|
|
||||||
lines.push("");
|
|
||||||
for (const block of monitorv2Blocks)
|
|
||||||
lines.push(block);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
const content = lines.join("\n");
|
const content = lines.join("\n");
|
||||||
|
|
||||||
Proc.runCommand("hypr-write-outputs", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
|
Proc.runCommand("hypr-write-outputs", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
|
||||||
@@ -196,17 +181,18 @@ Singleton {
|
|||||||
const gaps = (typeof SettingsData !== "undefined" && SettingsData.hyprlandLayoutGapsOverride >= 0) ? SettingsData.hyprlandLayoutGapsOverride : defaultGaps;
|
const gaps = (typeof SettingsData !== "undefined" && SettingsData.hyprlandLayoutGapsOverride >= 0) ? SettingsData.hyprlandLayoutGapsOverride : defaultGaps;
|
||||||
const borderSize = (typeof SettingsData !== "undefined" && SettingsData.hyprlandLayoutBorderSize >= 0) ? SettingsData.hyprlandLayoutBorderSize : defaultBorderSize;
|
const borderSize = (typeof SettingsData !== "undefined" && SettingsData.hyprlandLayoutBorderSize >= 0) ? SettingsData.hyprlandLayoutBorderSize : defaultBorderSize;
|
||||||
|
|
||||||
let content = `# Auto-generated by DMS - do not edit manually
|
let content = `-- Auto-generated by DMS — do not edit manually
|
||||||
|
|
||||||
general {
|
hl.config({
|
||||||
gaps_in = ${gaps}
|
general = {
|
||||||
gaps_out = ${gaps}
|
gaps_in = ${gaps},
|
||||||
border_size = ${borderSize}
|
gaps_out = ${gaps},
|
||||||
}
|
border_size = ${borderSize},
|
||||||
|
},
|
||||||
decoration {
|
decoration = {
|
||||||
rounding = ${cornerRadius}
|
rounding = ${cornerRadius},
|
||||||
}
|
},
|
||||||
|
})
|
||||||
`;
|
`;
|
||||||
|
|
||||||
Proc.runCommand("hypr-write-layout", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && cat > "${layoutPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
|
Proc.runCommand("hypr-write-layout", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && cat > "${layoutPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
|
||||||
@@ -271,7 +257,7 @@ decoration {
|
|||||||
|
|
||||||
const settings = typeof SettingsData !== "undefined" ? SettingsData.cursorSettings : null;
|
const settings = typeof SettingsData !== "undefined" ? SettingsData.cursorSettings : null;
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
Proc.runCommand("hypr-write-cursor", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && : > "${cursorPath}"`], (output, exitCode) => {
|
Proc.runCommand("hypr-write-cursor", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && printf '%s\\n' "-- Auto-generated by DMS — do not edit manually" "" > "${cursorPath}"`], (output, exitCode) => {
|
||||||
if (exitCode !== 0)
|
if (exitCode !== 0)
|
||||||
log.warn("Failed to write cursor config:", output);
|
log.warn("Failed to write cursor config:", output);
|
||||||
});
|
});
|
||||||
@@ -289,32 +275,34 @@ decoration {
|
|||||||
const hasCursorSettings = hideOnKeyPress || hideOnTouch || inactiveTimeout > 0;
|
const hasCursorSettings = hideOnKeyPress || hideOnTouch || inactiveTimeout > 0;
|
||||||
|
|
||||||
if (!hasTheme && !hasNonDefaultSize && !hasCursorSettings) {
|
if (!hasTheme && !hasNonDefaultSize && !hasCursorSettings) {
|
||||||
Proc.runCommand("hypr-write-cursor", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && : > "${cursorPath}"`], (output, exitCode) => {
|
Proc.runCommand("hypr-write-cursor", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && printf '%s\\n' "-- Auto-generated by DMS — do not edit manually" "" > "${cursorPath}"`], (output, exitCode) => {
|
||||||
if (exitCode !== 0)
|
if (exitCode !== 0)
|
||||||
log.warn("Failed to write cursor config:", output);
|
log.warn("Failed to write cursor config:", output);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lines = ["# Auto-generated by DMS - do not edit manually", ""];
|
let lines = ["-- Auto-generated by DMS — do not edit manually", ""];
|
||||||
|
|
||||||
if (hasTheme) {
|
if (hasTheme) {
|
||||||
lines.push(`env = HYPRCURSOR_THEME,${themeName}`);
|
lines.push(`hl.env("HYPRCURSOR_THEME", ${luaQuoted(themeName)})`);
|
||||||
lines.push(`env = XCURSOR_THEME,${themeName}`);
|
lines.push(`hl.env("XCURSOR_THEME", ${luaQuoted(themeName)})`);
|
||||||
}
|
}
|
||||||
lines.push(`env = HYPRCURSOR_SIZE,${size}`);
|
lines.push(`hl.env("HYPRCURSOR_SIZE", ${luaQuoted(String(size))})`);
|
||||||
lines.push(`env = XCURSOR_SIZE,${size}`);
|
lines.push(`hl.env("XCURSOR_SIZE", ${luaQuoted(String(size))})`);
|
||||||
|
|
||||||
if (hasCursorSettings) {
|
if (hasCursorSettings) {
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("cursor {");
|
lines.push("hl.config({");
|
||||||
|
lines.push("\tcursor = {");
|
||||||
if (hideOnKeyPress)
|
if (hideOnKeyPress)
|
||||||
lines.push(" hide_on_key_press = true");
|
lines.push("\t\thide_on_key_press = true,");
|
||||||
if (hideOnTouch)
|
if (hideOnTouch)
|
||||||
lines.push(" hide_on_touch = true");
|
lines.push("\t\thide_on_touch = true,");
|
||||||
if (inactiveTimeout > 0)
|
if (inactiveTimeout > 0)
|
||||||
lines.push(` inactive_timeout = ${inactiveTimeout}`);
|
lines.push(`\t\tinactive_timeout = ${inactiveTimeout},`);
|
||||||
lines.push("}");
|
lines.push("\t},");
|
||||||
|
lines.push("})");
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Quickshell
|
|||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
import "../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve
|
||||||
import "../Common/KeybindActions.js" as Actions
|
import "../Common/KeybindActions.js" as Actions
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
@@ -82,6 +83,7 @@ Singleton {
|
|||||||
case "niri":
|
case "niri":
|
||||||
return compositorConfigDir + "/dms/binds.kdl";
|
return compositorConfigDir + "/dms/binds.kdl";
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
|
return compositorConfigDir + "/dms/binds.lua";
|
||||||
case "mangowc":
|
case "mangowc":
|
||||||
return compositorConfigDir + "/dms/binds.conf";
|
return compositorConfigDir + "/dms/binds.conf";
|
||||||
default:
|
default:
|
||||||
@@ -93,7 +95,7 @@ Singleton {
|
|||||||
case "niri":
|
case "niri":
|
||||||
return compositorConfigDir + "/config.kdl";
|
return compositorConfigDir + "/config.kdl";
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
return compositorConfigDir + "/hyprland.conf";
|
return compositorConfigDir + "/hyprland.lua";
|
||||||
case "mangowc":
|
case "mangowc":
|
||||||
return compositorConfigDir + "/config.conf";
|
return compositorConfigDir + "/config.conf";
|
||||||
default:
|
default:
|
||||||
@@ -247,8 +249,8 @@ Singleton {
|
|||||||
root.lastError = "";
|
root.lastError = "";
|
||||||
root.dmsBindsIncluded = true;
|
root.dmsBindsIncluded = true;
|
||||||
root.dmsBindsFixed();
|
root.dmsBindsFixed();
|
||||||
const bindsFile = root.currentProvider === "niri" ? "dms/binds.kdl" : "dms/binds.conf";
|
const bindsRel = root.currentProvider === "niri" ? "dms/binds.kdl" : root.currentProvider === "hyprland" ? "dms/binds.lua" : "dms/binds.conf";
|
||||||
ToastService.showInfo(I18n.tr("Binds include added"), I18n.tr("%1 is now included in config").arg(bindsFile), "", "keybinds");
|
ToastService.showInfo(I18n.tr("Binds include added"), I18n.tr("%1 is now included in config").arg(bindsRel), "", "keybinds");
|
||||||
Qt.callLater(root.forceReload);
|
Qt.callLater(root.forceReload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,13 +264,36 @@ Singleton {
|
|||||||
let script;
|
let script;
|
||||||
switch (currentProvider) {
|
switch (currentProvider) {
|
||||||
case "niri":
|
case "niri":
|
||||||
script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.kdl" && cp "${mainConfigPath}" "${backupPath}" && echo 'include "dms/binds.kdl"' >> "${mainConfigPath}"`;
|
script = ConfigIncludeResolve.buildRepairScript({
|
||||||
|
configFile: mainConfigPath,
|
||||||
|
backupFile: backupPath,
|
||||||
|
fragmentFile: compositorConfigDir + "/dms/binds.kdl",
|
||||||
|
grepPattern: 'include.*"dms/binds.kdl"',
|
||||||
|
includeLine: 'include "dms/binds.kdl"'
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.conf" && cp "${mainConfigPath}" "${backupPath}" && echo 'source = ./dms/binds.conf' >> "${mainConfigPath}"`;
|
script = ConfigIncludeResolve.buildRepairScript({
|
||||||
|
configFile: mainConfigPath,
|
||||||
|
backupFile: backupPath,
|
||||||
|
fragmentFiles: [compositorConfigDir + "/dms/binds.lua", compositorConfigDir + "/dms/binds-user.lua"],
|
||||||
|
includes: [{
|
||||||
|
grepPattern: "dms.binds",
|
||||||
|
includeLine: "require(\"dms.binds\")"
|
||||||
|
}, {
|
||||||
|
grepPattern: "dms.binds-user",
|
||||||
|
includeLine: "require(\"dms.binds-user\")"
|
||||||
|
}]
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "mangowc":
|
case "mangowc":
|
||||||
script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.conf" && cp "${mainConfigPath}" "${backupPath}" && echo 'source = ./dms/binds.conf' >> "${mainConfigPath}"`;
|
script = ConfigIncludeResolve.buildRepairScript({
|
||||||
|
configFile: mainConfigPath,
|
||||||
|
backupFile: backupPath,
|
||||||
|
fragmentFile: compositorConfigDir + "/dms/binds.conf",
|
||||||
|
grepPattern: "source.*dms/binds.conf",
|
||||||
|
includeLine: "source = ./dms/binds.conf"
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
fixing = false;
|
fixing = false;
|
||||||
@@ -321,6 +346,7 @@ Singleton {
|
|||||||
"statusMessage": status.statusMessage ?? ""
|
"statusMessage": status.statusMessage ?? ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
_maybeWarnHyprlandLegacyConf();
|
||||||
|
|
||||||
if (!_rawData?.binds) {
|
if (!_rawData?.binds) {
|
||||||
_allBinds = {};
|
_allBinds = {};
|
||||||
@@ -365,10 +391,13 @@ Singleton {
|
|||||||
for (var i = 0; i < binds.length; i++) {
|
for (var i = 0; i < binds.length; i++) {
|
||||||
const bind = binds[i];
|
const bind = binds[i];
|
||||||
const action = bind.action || "";
|
const action = bind.action || "";
|
||||||
|
const sourceStr = bind.source || "config";
|
||||||
const keyData = {
|
const keyData = {
|
||||||
"key": bind.key || "",
|
"key": bind.key || "",
|
||||||
"source": bind.source || "config",
|
"source": sourceStr,
|
||||||
"isOverride": bind.source === "dms",
|
"isOverride": sourceStr === "dms",
|
||||||
|
"isDMSManaged": sourceStr === "dms" || sourceStr === "dms-default",
|
||||||
|
"hasDefault": bind.hasDefault === true,
|
||||||
"cooldownMs": bind.cooldownMs || 0,
|
"cooldownMs": bind.cooldownMs || 0,
|
||||||
"flags": bind.flags || "",
|
"flags": bind.flags || "",
|
||||||
"allowWhenLocked": bind.allowWhenLocked || false,
|
"allowWhenLocked": bind.allowWhenLocked || false,
|
||||||
@@ -456,6 +485,19 @@ Singleton {
|
|||||||
_pendingSavedKey = bindData.key;
|
_pendingSavedKey = bindData.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property bool _hyprlandLegacyWarnShown: false
|
||||||
|
|
||||||
|
function _maybeWarnHyprlandLegacyConf() {
|
||||||
|
if (_hyprlandLegacyWarnShown)
|
||||||
|
return;
|
||||||
|
if (currentProvider !== "hyprland")
|
||||||
|
return;
|
||||||
|
if (!dmsStatus.exists || dmsStatus.included)
|
||||||
|
return;
|
||||||
|
_hyprlandLegacyWarnShown = true;
|
||||||
|
ToastService.showWarning(I18n.tr("Hyprland config still uses hyprlang"), I18n.tr("DMS Settings now writes Lua. Edits won't apply until you migrate."), "dms setup", "hyprland-migration");
|
||||||
|
}
|
||||||
|
|
||||||
function removeBind(key) {
|
function removeBind(key) {
|
||||||
if (!key)
|
if (!key)
|
||||||
return;
|
return;
|
||||||
@@ -464,6 +506,14 @@ Singleton {
|
|||||||
bindRemoved(key);
|
bindRemoved(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetBind(key) {
|
||||||
|
if (!key)
|
||||||
|
return;
|
||||||
|
removeProcess.command = ["dms", "keybinds", "reset", currentProvider, key];
|
||||||
|
removeProcess.running = true;
|
||||||
|
bindRemoved(key);
|
||||||
|
}
|
||||||
|
|
||||||
function isDmsAction(action) {
|
function isDmsAction(action) {
|
||||||
return Actions.isDmsAction(action);
|
return Actions.isDmsAction(action);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,13 +66,12 @@ Item {
|
|||||||
signal toggleExpand
|
signal toggleExpand
|
||||||
signal saveBind(string originalKey, var newData)
|
signal saveBind(string originalKey, var newData)
|
||||||
signal removeBind(string key)
|
signal removeBind(string key)
|
||||||
|
signal resetBind(string key)
|
||||||
signal cancelEdit
|
signal cancelEdit
|
||||||
|
|
||||||
implicitHeight: contentColumn.implicitHeight
|
implicitHeight: contentColumn.implicitHeight
|
||||||
height: implicitHeight
|
height: implicitHeight
|
||||||
|
|
||||||
Component.onDestruction: _destroyShortcutInhibitor()
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (isNew && isExpanded)
|
if (isNew && isExpanded)
|
||||||
resetEdits();
|
resetEdits();
|
||||||
@@ -831,9 +830,12 @@ Item {
|
|||||||
color: root._actionType === modelData.id ? Theme.surfaceContainerHighest : Theme.surfaceContainer
|
color: root._actionType === modelData.id ? Theme.surfaceContainerHighest : Theme.surfaceContainer
|
||||||
border.color: root._actionType === modelData.id ? Theme.outline : (typeArea.containsMouse ? Theme.outlineVariant : "transparent")
|
border.color: root._actionType === modelData.id ? Theme.outline : (typeArea.containsMouse ? Theme.outlineVariant : "transparent")
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
clip: true
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.centerIn: parent
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
@@ -843,10 +845,13 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
text: typeDelegate.modelData.label
|
text: typeDelegate.modelData.label
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: root._actionType === typeDelegate.modelData.id ? Theme.surfaceText : Theme.surfaceVariantText
|
color: root._actionType === typeDelegate.modelData.id ? Theme.surfaceText : Theme.surfaceVariantText
|
||||||
visible: typeDelegate.width > 100
|
visible: typeDelegate.width > 100
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1763,10 +1768,19 @@ Item {
|
|||||||
iconName: "delete"
|
iconName: "delete"
|
||||||
iconSize: Theme.iconSize - 4
|
iconSize: Theme.iconSize - 4
|
||||||
iconColor: Theme.error
|
iconColor: Theme.error
|
||||||
visible: root.editingKeyIndex >= 0 && root.editingKeyIndex < root.keys.length && root.keys[root.editingKeyIndex].isOverride && !root.isNew
|
visible: root.editingKeyIndex >= 0 && root.editingKeyIndex < root.keys.length && (root.keys[root.editingKeyIndex].isDMSManaged || root.keys[root.editingKeyIndex].isOverride) && !root.isNew
|
||||||
onClicked: root.removeBind(root._originalKey)
|
onClicked: root.removeBind(root._originalKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: I18n.tr("Reset to default")
|
||||||
|
buttonHeight: root._buttonHeight
|
||||||
|
backgroundColor: Theme.surfaceContainer
|
||||||
|
textColor: Theme.primary
|
||||||
|
visible: root.editingKeyIndex >= 0 && root.editingKeyIndex < root.keys.length && root.keys[root.editingKeyIndex].isOverride === true && root.keys[root.editingKeyIndex].hasDefault === true && !root.isNew
|
||||||
|
onClicked: root.resetBind(root._originalKey)
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
[templates.dmshyprland]
|
[templates.dmshyprland]
|
||||||
input_path = 'SHELL_DIR/matugen/templates/hypr-colors.conf'
|
input_path = 'SHELL_DIR/matugen/templates/hypr-colors.lua'
|
||||||
output_path = 'CONFIG_DIR/hypr/dms/colors.conf'
|
output_path = 'CONFIG_DIR/hypr/dms/colors.lua'
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
# Auto-generated by DMS - do not edit manually
|
|
||||||
# Remove source = ./dms/colors.conf from your config to override.
|
|
||||||
|
|
||||||
$primary = rgb({{colors.primary.default.hex_stripped}})
|
|
||||||
$outline = rgb({{colors.outline.default.hex_stripped}})
|
|
||||||
$error = rgb({{colors.error.default.hex_stripped}})
|
|
||||||
|
|
||||||
general {
|
|
||||||
col.active_border = $primary
|
|
||||||
col.inactive_border = $outline
|
|
||||||
}
|
|
||||||
|
|
||||||
group {
|
|
||||||
col.border_active = $primary
|
|
||||||
col.border_inactive = $outline
|
|
||||||
col.border_locked_active = $error
|
|
||||||
col.border_locked_inactive = $outline
|
|
||||||
|
|
||||||
groupbar {
|
|
||||||
col.active = $primary
|
|
||||||
col.inactive = $outline
|
|
||||||
col.locked_active = $error
|
|
||||||
col.locked_inactive = $outline
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- Auto-generated by DMS Matugen hook — do not edit manually.
|
||||||
|
-- Remove require("dms.colors") from hyprland.lua to override.
|
||||||
|
|
||||||
|
hl.config({
|
||||||
|
general = {
|
||||||
|
col = {
|
||||||
|
active_border = "rgb({{colors.primary.default.hex_stripped}})",
|
||||||
|
inactive_border = "rgb({{colors.outline.default.hex_stripped}})",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
group = {
|
||||||
|
col = {
|
||||||
|
border_active = "rgb({{colors.primary.default.hex_stripped}})",
|
||||||
|
border_inactive = "rgb({{colors.outline.default.hex_stripped}})",
|
||||||
|
border_locked_active = "rgb({{colors.error.default.hex_stripped}})",
|
||||||
|
border_locked_inactive = "rgb({{colors.outline.default.hex_stripped}})",
|
||||||
|
},
|
||||||
|
groupbar = {
|
||||||
|
col = {
|
||||||
|
active = "rgb({{colors.primary.default.hex_stripped}})",
|
||||||
|
inactive = "rgb({{colors.outline.default.hex_stripped}})",
|
||||||
|
locked_active = "rgb({{colors.error.default.hex_stripped}})",
|
||||||
|
locked_inactive = "rgb({{colors.outline.default.hex_stripped}})",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user