mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-08 04:09:15 -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"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||
@@ -37,6 +38,7 @@ var runCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
log.ApplyEnvOverrides()
|
||||
config.CleanupStrayHyprlandConfFile(log.Infof)
|
||||
if daemon {
|
||||
runShellDaemon(session)
|
||||
} else {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/luaconfig"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -27,7 +28,21 @@ var resolveIncludeCmd = &cobra.Command{
|
||||
case 0:
|
||||
return []string{"hyprland", "niri", "mangowc"}, cobra.ShellCompDirectiveNoFileComp
|
||||
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
|
||||
},
|
||||
@@ -82,17 +97,35 @@ func checkHyprlandInclude(filename string) (IncludeResult, error) {
|
||||
result.Exists = true
|
||||
}
|
||||
|
||||
mainConfig := filepath.Join(configDir, "hyprland.conf")
|
||||
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||
return result, nil
|
||||
targetAbs, err := filepath.Abs(targetPath)
|
||||
if err != 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
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -141,7 +174,7 @@ func hyprlandFindInclude(filePath, target string, processed map[string]bool) boo
|
||||
continue
|
||||
}
|
||||
|
||||
if hyprlandFindInclude(expanded, target, processed) {
|
||||
if hyprlandFindIncludeHyprlang(expanded, target, processed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,12 +51,20 @@ var keybindsSetCmd = &cobra.Command{
|
||||
|
||||
var keybindsRemoveCmd = &cobra.Command{
|
||||
Use: "remove <provider> <key>",
|
||||
Short: "Remove a keybind override",
|
||||
Long: "Remove a keybind override from the specified provider",
|
||||
Short: "Remove a keybind",
|
||||
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),
|
||||
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() {
|
||||
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
|
||||
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
||||
@@ -72,6 +80,7 @@ func init() {
|
||||
keybindsCmd.AddCommand(keybindsShowCmd)
|
||||
keybindsCmd.AddCommand(keybindsSetCmd)
|
||||
keybindsCmd.AddCommand(keybindsRemoveCmd)
|
||||
keybindsCmd.AddCommand(keybindsResetCmd)
|
||||
|
||||
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
|
||||
return providers.NewJSONFileProvider(filePath)
|
||||
@@ -263,3 +272,19 @@ func runKeybindsRemove(_ *cobra.Command, args []string) {
|
||||
}, "", " ")
|
||||
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{
|
||||
"binds": {
|
||||
niriFile: "binds.kdl",
|
||||
hyprFile: "binds.conf",
|
||||
hyprFile: "binds.lua",
|
||||
niriContent: func(t string) string {
|
||||
return strings.ReplaceAll(config.NiriBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
||||
},
|
||||
hyprContent: func(t string) string {
|
||||
return strings.ReplaceAll(config.HyprBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
||||
return strings.ReplaceAll(config.DMSBindsLuaConfig, "{{TERMINAL_COMMAND}}", t)
|
||||
},
|
||||
},
|
||||
"layout": {
|
||||
niriFile: "layout.kdl",
|
||||
hyprFile: "layout.conf",
|
||||
hyprFile: "layout.lua",
|
||||
niriContent: func(_ string) string { return config.NiriLayoutConfig },
|
||||
hyprContent: func(_ string) string { return config.HyprLayoutConfig },
|
||||
hyprContent: func(_ string) string { return config.DMSLayoutLuaConfig },
|
||||
},
|
||||
"colors": {
|
||||
niriFile: "colors.kdl",
|
||||
hyprFile: "colors.conf",
|
||||
hyprFile: "colors.lua",
|
||||
niriContent: func(_ string) string { return config.NiriColorsConfig },
|
||||
hyprContent: func(_ string) string { return config.HyprColorsConfig },
|
||||
hyprContent: func(_ string) string { return config.DMSColorsLuaConfig },
|
||||
},
|
||||
"alttab": {
|
||||
niriFile: "alttab.kdl",
|
||||
@@ -135,21 +135,21 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{
|
||||
},
|
||||
"outputs": {
|
||||
niriFile: "outputs.kdl",
|
||||
hyprFile: "outputs.conf",
|
||||
hyprFile: "outputs.lua",
|
||||
niriContent: func(_ string) string { return "" },
|
||||
hyprContent: func(_ string) string { return "" },
|
||||
hyprContent: func(_ string) string { return config.DMSOutputsLuaConfig },
|
||||
},
|
||||
"cursor": {
|
||||
niriFile: "cursor.kdl",
|
||||
hyprFile: "cursor.conf",
|
||||
hyprFile: "cursor.lua",
|
||||
niriContent: func(_ string) string { return "" },
|
||||
hyprContent: func(_ string) string { return "" },
|
||||
hyprContent: func(_ string) string { return config.DMSCursorLuaConfig },
|
||||
},
|
||||
"windowrules": {
|
||||
niriFile: "windowrules.kdl",
|
||||
hyprFile: "windowrules.conf",
|
||||
hyprFile: "windowrules.lua",
|
||||
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
|
||||
|
||||
if wmSelected {
|
||||
var configPath string
|
||||
var configPaths []string
|
||||
switch wm {
|
||||
case deps.WindowManagerNiri:
|
||||
configPath = filepath.Join(homeDir, ".config", "niri", "config.kdl")
|
||||
configPaths = []string{filepath.Join(homeDir, ".config", "niri", "config.kdl")}
|
||||
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 {
|
||||
willBackup = true
|
||||
for _, configPath := range configPaths {
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
willBackup = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ var windowrulesListCmd = &cobra.Command{
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
@@ -40,8 +40,7 @@ var windowrulesAddCmd = &cobra.Command{
|
||||
Args: cobra.ExactArgs(2),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
// ! disabled hyprland return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
@@ -55,7 +54,7 @@ var windowrulesUpdateCmd = &cobra.Command{
|
||||
Args: cobra.ExactArgs(3),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
@@ -69,7 +68,7 @@ var windowrulesRemoveCmd = &cobra.Command{
|
||||
Args: cobra.ExactArgs(2),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
@@ -83,7 +82,7 @@ var windowrulesReorderCmd = &cobra.Command{
|
||||
Args: cobra.ExactArgs(2),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
@@ -118,9 +117,9 @@ func getCompositor(args []string) string {
|
||||
if os.Getenv("NIRI_SOCKET") != "" {
|
||||
return "niri"
|
||||
}
|
||||
// if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
||||
// return "hyprland"
|
||||
// }
|
||||
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
||||
return "hyprland"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -183,7 +182,6 @@ func runWindowrulesList(cmd *cobra.Command, args []string) {
|
||||
result.DMSStatus = parseResult.DMSStatus
|
||||
|
||||
case "hyprland":
|
||||
log.Fatalf("Hyprland support is currently disabled.") // ! disabled hyprland
|
||||
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to expand hyprland config path: %v", err)
|
||||
|
||||
+179
-110
@@ -12,6 +12,8 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
)
|
||||
|
||||
const hyprlandBackupDirName = ".dms-backups"
|
||||
|
||||
type ConfigDeployer struct {
|
||||
logChan chan<- string
|
||||
}
|
||||
@@ -63,12 +65,23 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
|
||||
var results []DeploymentResult
|
||||
|
||||
// Primary config file paths used to detect fresh installs.
|
||||
configPrimaryPaths := map[string]string{
|
||||
"Niri": filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
|
||||
"Hyprland": 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"),
|
||||
configPrimaryPaths := map[string][]string{
|
||||
"Niri": {
|
||||
filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
|
||||
},
|
||||
"Hyprland": {
|
||||
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 {
|
||||
@@ -81,8 +94,15 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
|
||||
}
|
||||
// Config is explicitly set to "don't replace" — but still deploy
|
||||
// if the config file doesn't exist yet (fresh install scenario).
|
||||
if primaryPath, ok := configPrimaryPaths[configType]; ok {
|
||||
if _, err := os.Stat(primaryPath); os.IsNotExist(err) {
|
||||
if primaryPaths, ok := configPrimaryPaths[configType]; ok {
|
||||
exists := false
|
||||
for _, primaryPath := range primaryPaths {
|
||||
if _, err := os.Stat(primaryPath); err == nil {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -495,7 +515,7 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dms
|
||||
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
|
||||
result := DeploymentResult{
|
||||
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)
|
||||
@@ -510,20 +530,20 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
backupDir := filepath.Join(configDir, hyprlandBackupDirName, timestamp)
|
||||
var existingConfig string
|
||||
if _, err := os.Stat(result.Path); err == nil {
|
||||
cd.log("Found existing Hyprland configuration")
|
||||
existingData, existingPath, err := readExistingHyprlandConfig(configDir)
|
||||
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)
|
||||
if 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.BackupPath = filepath.Join(backupDir, filepath.Base(existingPath))
|
||||
if err := backupHyprlandConfigFile(existingPath, result.BackupPath, []byte(existingData), strings.EqualFold(filepath.Ext(existingPath), ".conf")); err != nil {
|
||||
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
@@ -542,10 +562,10 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
||||
terminalCommand = "ghostty"
|
||||
}
|
||||
|
||||
newConfig := strings.ReplaceAll(HyprlandConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||
newConfig := strings.ReplaceAll(HyprlandLuaConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||
|
||||
if !useSystemd {
|
||||
newConfig = cd.transformHyprlandConfigForNonSystemd(newConfig, terminalCommand)
|
||||
newConfig = transformHyprlandLuaForNonSystemd(newConfig, terminalCommand)
|
||||
}
|
||||
|
||||
if existingConfig != "" {
|
||||
@@ -563,6 +583,18 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
||||
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 {
|
||||
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
||||
return result, result.Error
|
||||
@@ -573,29 +605,118 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
||||
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 {
|
||||
configs := []struct {
|
||||
name string
|
||||
content string
|
||||
name string
|
||||
content string
|
||||
overwrite bool
|
||||
}{
|
||||
{"colors.conf", HyprColorsConfig},
|
||||
{"layout.conf", HyprLayoutConfig},
|
||||
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||
{"outputs.conf", ""},
|
||||
{"cursor.conf", ""},
|
||||
{"windowrules.conf", ""},
|
||||
{name: "colors.lua", content: DMSColorsLuaConfig},
|
||||
{name: "layout.lua", content: DMSLayoutLuaConfig},
|
||||
{name: "binds.lua", content: strings.ReplaceAll(DMSBindsLuaConfig, "{{TERMINAL_COMMAND}}", terminalCommand), overwrite: true},
|
||||
{name: "binds-user.lua", content: DMSBindsUserLuaConfig},
|
||||
{name: "outputs.lua", content: DMSOutputsLuaConfig},
|
||||
{name: "cursor.lua", content: DMSCursorLuaConfig},
|
||||
{name: "windowrules.lua", content: DMSWindowRulesLuaConfig},
|
||||
}
|
||||
|
||||
for _, cfg := range configs {
|
||||
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 {
|
||||
existed = true
|
||||
}
|
||||
if existed && !cfg.overwrite {
|
||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||
continue
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -603,94 +724,42 @@ func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalComman
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir string) (string, error) {
|
||||
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
|
||||
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
|
||||
|
||||
if len(existingMonitors) == 0 {
|
||||
_ = newConfig
|
||||
lines := extractHyprlangMonitorLines(existingConfig)
|
||||
if len(lines) == 0 {
|
||||
return newConfig, nil
|
||||
}
|
||||
|
||||
outputsPath := filepath.Join(dmsDir, "outputs.conf")
|
||||
if _, err := os.Stat(outputsPath); err != nil {
|
||||
var outputsContent strings.Builder
|
||||
for _, monitor := range existingMonitors {
|
||||
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")
|
||||
}
|
||||
outputsPath := filepath.Join(dmsDir, "outputs.lua")
|
||||
if info, err := os.Stat(outputsPath); err == nil && info.Size() > 0 {
|
||||
cd.log("Skipping monitor migration: dms/outputs.lua already exists")
|
||||
return newConfig, nil
|
||||
}
|
||||
|
||||
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
|
||||
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
|
||||
|
||||
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
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("-- Migrated from existing hyprlang monitor lines\n\n")
|
||||
ok := 0
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "exec-once = dbus-update-activation-environment") {
|
||||
lua, err := hyprlangMonitorLineToLua(line)
|
||||
if err != nil {
|
||||
cd.log(fmt.Sprintf("Warning: could not migrate monitor line %q: %v", line, err))
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
|
||||
startupSectionFound = true
|
||||
result = append(result, "exec-once = dms run")
|
||||
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)
|
||||
b.WriteString(lua)
|
||||
b.WriteByte('\n')
|
||||
ok++
|
||||
}
|
||||
|
||||
if !startupSectionFound {
|
||||
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
|
||||
}
|
||||
}
|
||||
if ok == 0 {
|
||||
return newConfig, nil
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
b.WriteByte('\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 {
|
||||
|
||||
@@ -259,130 +259,56 @@ func getGhosttyPath() string {
|
||||
func TestMergeHyprlandMonitorSections(t *testing.T) {
|
||||
cd := &ConfigDeployer{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
newConfig string
|
||||
existingConfig string
|
||||
wantError bool
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
}{
|
||||
{
|
||||
name: "no existing monitors",
|
||||
newConfig: `# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
t.Run("no monitors in existing", func(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
out, err := cd.mergeHyprlandMonitorSections(`hl.config({})`, `input { kb_layout = us }`, tmp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `hl.config({})`, out)
|
||||
_, e := os.Stat(filepath.Join(tmp, "outputs.lua"))
|
||||
assert.True(t, os.IsNotExist(e))
|
||||
})
|
||||
|
||||
# ==================
|
||||
# ENVIRONMENT VARS
|
||||
# ==================
|
||||
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
|
||||
t.Run("writes outputs lua from hyprlang monitors", func(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
existing := `monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1
|
||||
monitor = eDP-1, 2560x1440@165, auto, 1.25`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"monitor = DP-1",
|
||||
"# monitor = HDMI-A-1", // Commented monitor preserved
|
||||
"monitor = eDP-1",
|
||||
"Monitors from existing configuration",
|
||||
},
|
||||
wantNotContains: []string{
|
||||
"monitor = eDP-2", // Example monitor should be removed
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preserve commented monitors",
|
||||
newConfig: `# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
monitor = eDP-1, 2560x1440@165, auto, 1.25`
|
||||
out, err := cd.mergeHyprlandMonitorSections(`return`, existing, tmp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `return`, out)
|
||||
b, err := os.ReadFile(filepath.Join(tmp, "outputs.lua"))
|
||||
require.NoError(t, err)
|
||||
s := string(b)
|
||||
assert.Contains(t, s, "hl.monitor")
|
||||
assert.Contains(t, s, "DP-1")
|
||||
assert.Contains(t, s, "HDMI-A-1")
|
||||
assert.Contains(t, s, "eDP-1")
|
||||
assert.Contains(t, s, "preferred") // fallback rule at end
|
||||
})
|
||||
|
||||
# ==================`,
|
||||
existingConfig: `# monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"# monitor = DP-1",
|
||||
"# monitor = HDMI-A-1",
|
||||
"Monitors from existing configuration",
|
||||
},
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
}
|
||||
t.Run("skips when outputs lua already exists", func(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
path := filepath.Join(tmp, "outputs.lua")
|
||||
require.NoError(t, os.WriteFile(path, []byte("-- keep\n"), 0o644))
|
||||
_, err := cd.mergeHyprlandMonitorSections(`x`, `monitor = DP-1, 1920x1080@144, 0x0, 1`, tmp)
|
||||
require.NoError(t, err)
|
||||
b, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "-- keep\n", string(b))
|
||||
})
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig, tmpDir)
|
||||
func TestHyprlangMonitorLineToLuaPreservesOptions(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`)
|
||||
require.NoError(t, err)
|
||||
|
||||
if tt.wantError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
assert.Contains(t, got, `output = "DP-1"`)
|
||||
assert.Contains(t, got, `transform = 1`)
|
||||
assert.Contains(t, got, `vrr = 2`)
|
||||
assert.Contains(t, got, `bitdepth = 10`)
|
||||
assert.Contains(t, got, `cm = "hdr"`)
|
||||
assert.Contains(t, got, `sdrbrightness = 1.2`)
|
||||
assert.Contains(t, got, `sdrsaturation = 0.98`)
|
||||
}
|
||||
|
||||
func TestHyprlandConfigDeployment(t *testing.T) {
|
||||
@@ -398,6 +324,10 @@ func TestHyprlandConfigDeployment(t *testing.T) {
|
||||
cd := NewConfigDeployer(logChan)
|
||||
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -408,12 +338,16 @@ func TestHyprlandConfigDeployment(t *testing.T) {
|
||||
|
||||
content, err := os.ReadFile(result.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
||||
assert.Contains(t, string(content), "source = ./dms/binds.conf")
|
||||
assert.Contains(t, string(content), "exec-once = ")
|
||||
assert.Contains(t, string(content), `require("dms.binds")`)
|
||||
assert.Contains(t, string(content), "DMS_STARTUP_BEGIN")
|
||||
assert.Contains(t, string(content), "hl.config(")
|
||||
})
|
||||
|
||||
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
|
||||
monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
monitor = HDMI-A-1, 3840x2160@60, 1920x0, 1.5
|
||||
@@ -422,11 +356,17 @@ general {
|
||||
gaps_in = 10
|
||||
}
|
||||
`
|
||||
hyprPath := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
||||
err := os.MkdirAll(filepath.Dir(hyprPath), 0o755)
|
||||
hyprPath := filepath.Join(td, ".config", "hypr", "hyprland.conf")
|
||||
err = os.MkdirAll(filepath.Dir(hyprPath), 0o755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(hyprPath, []byte(existingContent), 0o644)
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
@@ -440,13 +380,76 @@ general {
|
||||
backupContent, err := os.ReadFile(result.BackupPath)
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
|
||||
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
|
||||
assert.Contains(t, string(newContent), "source = ./dms/binds.conf")
|
||||
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
||||
assert.Contains(t, string(newContent), `require("dms.binds")`)
|
||||
|
||||
outputsPath := filepath.Join(td, ".config", "hypr", "dms", "outputs.lua")
|
||||
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) {
|
||||
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
||||
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
||||
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
||||
assert.Contains(t, HyprlandConfig, "source = ./dms/binds.conf")
|
||||
assert.Contains(t, HyprlandLuaConfig, `require("dms.binds")`)
|
||||
assert.Contains(t, HyprlandLuaConfig, "DMS_STARTUP_BEGIN")
|
||||
assert.Contains(t, HyprlandLuaConfig, "hl.config(")
|
||||
assert.Contains(t, HyprlandLuaConfig, "input =")
|
||||
}
|
||||
|
||||
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")
|
||||
})
|
||||
|
||||
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"
|
||||
|
||||
//go:embed embedded/hyprland.conf
|
||||
var HyprlandConfig string
|
||||
//go:embed embedded/hyprland.lua
|
||||
var HyprlandLuaConfig string
|
||||
|
||||
//go:embed embedded/hypr-colors.conf
|
||||
var HyprColorsConfig string
|
||||
//go:embed embedded/hypr-colors.lua
|
||||
var DMSColorsLuaConfig string
|
||||
|
||||
//go:embed embedded/hypr-layout.conf
|
||||
var HyprLayoutConfig string
|
||||
//go:embed embedded/hypr-layout.lua
|
||||
var DMSLayoutLuaConfig string
|
||||
|
||||
//go:embed embedded/hypr-binds.conf
|
||||
var HyprBindsConfig string
|
||||
//go:embed embedded/hypr-binds.lua
|
||||
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"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||
@@ -48,7 +49,7 @@ func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||
h.parsed = true
|
||||
|
||||
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{
|
||||
Title: "Hyprland Keybinds",
|
||||
@@ -88,7 +89,7 @@ func (h *HyprlandProvider) HasDMSBindsIncluded() bool {
|
||||
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
|
||||
if section.Name != "" {
|
||||
currentSubcat = section.Name
|
||||
@@ -96,12 +97,12 @@ func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory
|
||||
|
||||
for _, kb := range section.Keybinds {
|
||||
category := h.categorizeByDispatcher(kb.Dispatcher)
|
||||
bind := h.convertKeybind(&kb, currentSubcat, conflicts)
|
||||
bind := h.convertKeybind(&kb, currentSubcat, conflicts, defaultKeys)
|
||||
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
||||
}
|
||||
|
||||
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)
|
||||
rawAction := h.formatRawAction(kb.Dispatcher, kb.Params)
|
||||
desc := kb.Comment
|
||||
@@ -143,8 +144,15 @@ func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory st
|
||||
}
|
||||
|
||||
source := "config"
|
||||
if strings.Contains(kb.Source, "dms/binds.conf") {
|
||||
if isDMSBindsUserOverridePath(kb.Source) {
|
||||
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{
|
||||
@@ -154,9 +162,10 @@ func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory st
|
||||
Subcategory: subcategory,
|
||||
Source: source,
|
||||
Flags: kb.Flags,
|
||||
HasDefault: hasDefault,
|
||||
}
|
||||
|
||||
if source == "dms" && conflicts != nil {
|
||||
if (source == "dms" || source == "dms-default") && conflicts != nil {
|
||||
normalizedKey := strings.ToLower(keyStr)
|
||||
if conflictKb, ok := conflicts[normalizedKey]; ok {
|
||||
bind.Conflict = &keybinds.Keybind{
|
||||
@@ -188,9 +197,9 @@ func (h *HyprlandProvider) formatKey(kb *HyprlandKeyBinding) string {
|
||||
func (h *HyprlandProvider) GetOverridePath() string {
|
||||
expanded, err := utils.ExpandPath(h.configPath)
|
||||
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 {
|
||||
@@ -250,7 +259,16 @@ func (h *HyprlandProvider) RemoveBind(key string) error {
|
||||
if err != 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)
|
||||
delete(existingBinds, normalizedKey)
|
||||
return h.writeOverrideBinds(existingBinds)
|
||||
@@ -262,116 +280,12 @@ type hyprlandOverrideBind struct {
|
||||
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
|
||||
Options map[string]any
|
||||
// Unbind: negative override (hl.unbind only, no rebind).
|
||||
Unbind bool
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind, error) {
|
||||
overridePath := 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, "+")
|
||||
return readLuaOrHyprlangOverride(h.GetOverridePath())
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) getBindSortPriority(action string) int {
|
||||
@@ -420,78 +334,203 @@ func (h *HyprlandProvider) generateBindsContent(binds map[string]*hyprlandOverri
|
||||
})
|
||||
|
||||
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 {
|
||||
h.writeBindLine(&sb, bind)
|
||||
writeLuaBindLine(&sb, bind)
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) writeBindLine(sb *strings.Builder, bind *hyprlandOverrideBind) {
|
||||
mods, key := h.parseKeyString(bind.Key)
|
||||
dispatcher, params := h.parseAction(bind.Action)
|
||||
|
||||
// Write bind type with flags (e.g., "bind", "binde", "bindel")
|
||||
sb.WriteString("bind")
|
||||
if bind.Flags != "" {
|
||||
sb.WriteString(bind.Flags)
|
||||
func formatLuaBindKey(internalKey string) string {
|
||||
internalKey = strings.TrimSpace(internalKey)
|
||||
parts := strings.Split(internalKey, "+")
|
||||
for i := range parts {
|
||||
parts[i] = normalizeLuaBindKeyPart(strings.TrimSpace(parts[i]))
|
||||
}
|
||||
sb.WriteString(" = ")
|
||||
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")
|
||||
return strings.Join(parts, " + ")
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) parseKeyString(keyStr string) (mods, key string) {
|
||||
parts := strings.Split(keyStr, "+")
|
||||
switch len(parts) {
|
||||
case 0:
|
||||
return "", keyStr
|
||||
case 1:
|
||||
return "", parts[0]
|
||||
func normalizeLuaBindKeyPart(part string) string {
|
||||
switch strings.ToLower(part) {
|
||||
case "super", "mod4", "mainmod":
|
||||
return "SUPER"
|
||||
case "ctrl", "control":
|
||||
return "CTRL"
|
||||
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:
|
||||
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) {
|
||||
parts := strings.SplitN(action, " ", 2)
|
||||
switch len(parts) {
|
||||
case 0:
|
||||
return action, ""
|
||||
case 1:
|
||||
dispatcher = parts[0]
|
||||
default:
|
||||
dispatcher = parts[0]
|
||||
params = parts[1]
|
||||
func luaExprToInternalAction(expr string) string {
|
||||
d, p := luaExprToDispatcherParams(expr)
|
||||
if d == "exec" && p != "" && !strings.HasPrefix(p, "hyprctl dispatch lua:") {
|
||||
return "exec " + p
|
||||
}
|
||||
|
||||
// Convert internal spawn format to Hyprland's exec
|
||||
if dispatcher == "spawn" {
|
||||
dispatcher = "exec"
|
||||
if p != "" {
|
||||
return d + " " + p
|
||||
}
|
||||
|
||||
return dispatcher, params
|
||||
return d
|
||||
}
|
||||
|
||||
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"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/luaconfig"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
)
|
||||
|
||||
@@ -50,6 +52,8 @@ type HyprlandParser struct {
|
||||
bindOrder []string
|
||||
processedFiles map[string]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 {
|
||||
@@ -64,6 +68,8 @@ func NewHyprlandParser(configDir string) *HyprlandParser {
|
||||
bindMap: make(map[string]*HyprlandKeyBinding),
|
||||
bindOrder: []string{},
|
||||
processedFiles: make(map[string]bool),
|
||||
removedKeys: make(map[string]bool),
|
||||
defaultDMSKeys: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,6 +298,7 @@ type HyprlandParseResult struct {
|
||||
DMSBindsIncluded bool
|
||||
DMSStatus *HyprlandDMSStatus
|
||||
ConflictingConfigs map[string]*HyprlandKeyBinding
|
||||
DefaultDMSKeys map[string]bool // keys with a DMS default in binds.{lua,conf}
|
||||
}
|
||||
|
||||
type HyprlandDMSStatus struct {
|
||||
@@ -317,10 +324,10 @@ func (p *HyprlandParser) buildDMSStatus() *HyprlandDMSStatus {
|
||||
switch {
|
||||
case !p.dmsBindsExists:
|
||||
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:
|
||||
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:
|
||||
status.Effective = true
|
||||
status.OverriddenBy = p.bindsAfterDMS
|
||||
@@ -347,8 +354,11 @@ func (p *HyprlandParser) normalizeKey(key string) string {
|
||||
func (p *HyprlandParser) addBind(kb *HyprlandKeyBinding) bool {
|
||||
key := p.formatBindKey(kb)
|
||||
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 {
|
||||
p.dmsBindKeys[normalizedKey] = true
|
||||
} else if p.dmsBindKeys[normalizedKey] {
|
||||
@@ -373,12 +383,21 @@ func (p *HyprlandParser) ParseWithDMS() (*HyprlandSection, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dmsBindsPath := filepath.Join(expandedDir, "dms", "binds.conf")
|
||||
if _, err := os.Stat(dmsBindsPath); err == nil {
|
||||
dmsBindsLua := filepath.Join(expandedDir, "dms", "binds.lua")
|
||||
dmsBindsConf := filepath.Join(expandedDir, "dms", "binds.conf")
|
||||
dmsBindsPath := ""
|
||||
if _, err := os.Stat(dmsBindsLua); err == nil {
|
||||
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, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -387,10 +406,65 @@ func (p *HyprlandParser) ParseWithDMS() (*HyprlandSection, error) {
|
||||
if p.dmsBindsExists && !p.dmsProcessed {
|
||||
p.parseDMSBindsDirectly(dmsBindsPath, section)
|
||||
}
|
||||
p.removeShadowedDMSBinds(section)
|
||||
p.removeUnboundDMSBinds(section)
|
||||
|
||||
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) {
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
@@ -407,6 +481,10 @@ func (p *HyprlandParser) parseFileWithSource(filePath, sectionName string) (*Hyp
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.EqualFold(filepath.Ext(absPath), ".lua") {
|
||||
return p.parseLuaLines(string(data), filepath.Dir(absPath), absPath, sectionName)
|
||||
}
|
||||
|
||||
prevSource := p.currentSource
|
||||
p.currentSource = absPath
|
||||
|
||||
@@ -446,7 +524,7 @@ func (p *HyprlandParser) handleSource(line string, section *HyprlandSection, bas
|
||||
}
|
||||
|
||||
sourcePath := strings.TrimSpace(parts[1])
|
||||
isDMSSource := sourcePath == "dms/binds.conf" || strings.HasSuffix(sourcePath, "/dms/binds.conf")
|
||||
isDMSSource := isDMSBindsPrimarySourcePath(sourcePath)
|
||||
|
||||
p.includeCount++
|
||||
if isDMSSource {
|
||||
@@ -474,6 +552,17 @@ func (p *HyprlandParser) handleSource(line string, section *HyprlandSection, bas
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -503,6 +592,124 @@ func (p *HyprlandParser) parseDMSBindsDirectly(dmsBindsPath string, section *Hyp
|
||||
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 {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) < 2 {
|
||||
@@ -623,5 +830,356 @@ func ParseHyprlandKeysWithDMS(path string) (*HyprlandParseResult, error) {
|
||||
DMSBindsIncluded: parser.dmsBindsIncluded,
|
||||
DMSStatus: parser.buildDMSStatus(),
|
||||
ConflictingConfigs: parser.conflictingConfigs,
|
||||
DefaultDMSKeys: parser.defaultDMSKeys,
|
||||
}, 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 (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||
)
|
||||
|
||||
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) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -141,7 +141,7 @@ func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding, conflicts map[st
|
||||
|
||||
source := "config"
|
||||
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{
|
||||
@@ -151,7 +151,7 @@ func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding, conflicts map[st
|
||||
Source: source,
|
||||
}
|
||||
|
||||
if source == "dms" && conflicts != nil {
|
||||
if source == "dms-default" && conflicts != nil {
|
||||
normalizedKey := strings.ToLower(keyStr)
|
||||
if conflictKb, ok := conflicts[normalizedKey]; ok {
|
||||
bind.Conflict = &keybinds.Keybind{
|
||||
@@ -249,6 +249,10 @@ func (m *MangoWCProvider) RemoveBind(key string) error {
|
||||
return m.writeOverrideBinds(existingBinds)
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) ResetBind(key string) error {
|
||||
return m.RemoveBind(key)
|
||||
}
|
||||
|
||||
type mangowcOverrideBind struct {
|
||||
Key string
|
||||
Action string
|
||||
|
||||
@@ -149,7 +149,7 @@ func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, co
|
||||
|
||||
source := "config"
|
||||
if strings.Contains(kb.Source, "dms/binds.kdl") {
|
||||
source = "dms"
|
||||
source = "dms-default"
|
||||
}
|
||||
|
||||
bind := keybinds.Keybind{
|
||||
@@ -165,7 +165,7 @@ func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, co
|
||||
Repeat: kb.Repeat,
|
||||
}
|
||||
|
||||
if source == "dms" && conflicts != nil {
|
||||
if source == "dms-default" && conflicts != nil {
|
||||
if conflictKb, ok := conflicts[keyStr]; ok {
|
||||
bind.Conflict = &keybinds.Keybind{
|
||||
Key: keyStr,
|
||||
@@ -269,6 +269,10 @@ func (n *NiriProvider) RemoveBind(key string) error {
|
||||
return n.writeOverrideBinds(existingBinds)
|
||||
}
|
||||
|
||||
func (n *NiriProvider) ResetBind(key string) error {
|
||||
return n.RemoveBind(key)
|
||||
}
|
||||
|
||||
type overrideBind struct {
|
||||
Key string
|
||||
Action string
|
||||
|
||||
@@ -13,6 +13,7 @@ type Keybind struct {
|
||||
AllowInhibiting *bool `json:"allowInhibiting,omitempty"` // nil=default(true), false=explicitly disabled
|
||||
Repeat *bool `json:"repeat,omitempty"` // nil=default(true), false=explicitly disabled
|
||||
Conflict *Keybind `json:"conflict,omitempty"`
|
||||
HasDefault bool `json:"hasDefault,omitempty"` // override has a DMS default to revert to
|
||||
}
|
||||
|
||||
type DMSBindsStatus struct {
|
||||
@@ -42,6 +43,11 @@ type Provider interface {
|
||||
type WritableProvider interface {
|
||||
Provider
|
||||
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
|
||||
// ResetBind reverts a user override to its DMS default. On single-file
|
||||
// providers this aliases to RemoveBind.
|
||||
ResetBind(key string) error
|
||||
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,
|
||||
})
|
||||
} 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
|
||||
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
|
||||
}
|
||||
configs = append(configs, ExistingConfigInfo{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,10 @@ package providers
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
|
||||
)
|
||||
|
||||
func TestParseWindowRuleV1(t *testing.T) {
|
||||
@@ -151,7 +154,7 @@ func TestHyprlandWritableProvider(t *testing.T) {
|
||||
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 {
|
||||
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) {
|
||||
if boolToInt(true) != 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)
|
||||
|
||||
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`
|
||||
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`
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Deprecated: greetd expects Hyprland 0.55+ Lua; use `/etc/greetd/dms-hypr.lua` instead.
|
||||
env = DMS_RUN_GREETER,1
|
||||
|
||||
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 EGL_PLATFORM=gbm
|
||||
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
|
||||
exec Hyprland -c /etc/greetd/dms-hypr.conf
|
||||
exec Hyprland -c /etc/greetd/dms-hypr.lua
|
||||
fi
|
||||
|
||||
@@ -7,6 +7,7 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import "../../../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -1074,10 +1075,86 @@ Singleton {
|
||||
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) {
|
||||
const result = {};
|
||||
const lines = content.split("\n");
|
||||
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*$/);
|
||||
if (disableMatch) {
|
||||
const name = disableMatch[1].trim();
|
||||
@@ -1269,10 +1346,10 @@ Singleton {
|
||||
};
|
||||
case "hyprland":
|
||||
return {
|
||||
"configFile": configDir + "/hypr/hyprland.conf",
|
||||
"outputsFile": configDir + "/hypr/dms/outputs.conf",
|
||||
"grepPattern": 'source.*dms/outputs.conf',
|
||||
"includeLine": "source = ./dms/outputs.conf"
|
||||
"configFile": configDir + "/hypr/hyprland.lua",
|
||||
"outputsFile": configDir + "/hypr/dms/outputs.lua",
|
||||
"grepPattern": "dms.outputs",
|
||||
"includeLine": "require(\"dms.outputs\")"
|
||||
};
|
||||
case "dwl":
|
||||
return {
|
||||
@@ -1296,7 +1373,7 @@ Singleton {
|
||||
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;
|
||||
|
||||
checkingInclude = true;
|
||||
@@ -1326,11 +1403,17 @@ Singleton {
|
||||
return;
|
||||
|
||||
fixingInclude = true;
|
||||
const outputsDir = paths.outputsFile.substring(0, paths.outputsFile.lastIndexOf("/"));
|
||||
const unixTime = Math.floor(Date.now() / 1000);
|
||||
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;
|
||||
if (exitCode !== 0)
|
||||
return;
|
||||
|
||||
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
@@ -96,6 +97,32 @@ Item {
|
||||
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() {
|
||||
if (showingNewBind) {
|
||||
showingNewBind = false;
|
||||
@@ -129,6 +156,10 @@ Item {
|
||||
onTriggered: keybindsTab._updateFiltered()
|
||||
}
|
||||
|
||||
ConfirmModal {
|
||||
id: removeBindConfirm
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: KeybindsService
|
||||
function onBindsLoaded() {
|
||||
@@ -238,7 +269,7 @@ Item {
|
||||
}
|
||||
|
||||
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)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
@@ -336,7 +367,7 @@ Item {
|
||||
}
|
||||
|
||||
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: {
|
||||
if (warningBox.showSetup)
|
||||
return I18n.tr("Click 'Setup' to create %1 and add include to config.").arg(bindsFile);
|
||||
@@ -623,8 +654,11 @@ Item {
|
||||
}
|
||||
onRemoveBind: key => {
|
||||
const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? "";
|
||||
KeybindsService.removeBind(key);
|
||||
keybindsTab._editingKey = remainingKey;
|
||||
keybindsTab.confirmRemoveBind(key, remainingKey);
|
||||
}
|
||||
onResetBind: key => {
|
||||
const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? "";
|
||||
keybindsTab.confirmResetBind(key, remainingKey);
|
||||
}
|
||||
onIsExpandedChanged: {
|
||||
if (!isExpanded || !keybindsTab._editingKey)
|
||||
|
||||
@@ -7,6 +7,7 @@ import qs.Modals.FileBrowser
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Settings.Widgets
|
||||
import "../../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve
|
||||
|
||||
Item {
|
||||
id: themeColorsTab
|
||||
@@ -39,10 +40,10 @@ Item {
|
||||
};
|
||||
case "hyprland":
|
||||
return {
|
||||
"configFile": configDir + "/hypr/hyprland.conf",
|
||||
"cursorFile": configDir + "/hypr/dms/cursor.conf",
|
||||
"grepPattern": 'source.*dms/cursor.conf',
|
||||
"includeLine": "source = ./dms/cursor.conf"
|
||||
"configFile": configDir + "/hypr/hyprland.lua",
|
||||
"cursorFile": configDir + "/hypr/dms/cursor.lua",
|
||||
"grepPattern": "dms.cursor",
|
||||
"includeLine": "require(\"dms.cursor\")"
|
||||
};
|
||||
case "dwl":
|
||||
return {
|
||||
@@ -66,7 +67,7 @@ Item {
|
||||
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;
|
||||
|
||||
checkingCursorInclude = true;
|
||||
@@ -95,10 +96,16 @@ Item {
|
||||
if (!paths)
|
||||
return;
|
||||
fixingCursorInclude = true;
|
||||
const cursorDir = paths.cursorFile.substring(0, paths.cursorFile.lastIndexOf("/"));
|
||||
const unixTime = Math.floor(Date.now() / 1000);
|
||||
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;
|
||||
if (exitCode !== 0)
|
||||
return;
|
||||
|
||||
@@ -8,6 +8,7 @@ import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -54,10 +55,10 @@ Item {
|
||||
};
|
||||
case "hyprland":
|
||||
return {
|
||||
"configFile": configDir + "/hypr/hyprland.conf",
|
||||
"rulesFile": configDir + "/hypr/dms/windowrules.conf",
|
||||
"grepPattern": 'source.*dms/windowrules.conf',
|
||||
"includeLine": "source = ./dms/windowrules.conf"
|
||||
"configFile": configDir + "/hypr/hyprland.lua",
|
||||
"rulesFile": configDir + "/hypr/dms/windowrules.lua",
|
||||
"grepPattern": "dms.windowrules",
|
||||
"includeLine": "require(\"dms.windowrules\")"
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
@@ -135,7 +136,7 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = (compositor === "niri") ? "windowrules.kdl" : "windowrules.conf";
|
||||
const filename = (compositor === "niri") ? "windowrules.kdl" : "windowrules.lua";
|
||||
checkingInclude = true;
|
||||
Proc.runCommand("check-windowrules-include", ["dms", "config", "resolve-include", compositor, filename], (output, exitCode) => {
|
||||
checkingInclude = false;
|
||||
@@ -162,10 +163,16 @@ Item {
|
||||
if (!paths)
|
||||
return;
|
||||
fixingInclude = true;
|
||||
const rulesDir = paths.rulesFile.substring(0, paths.rulesFile.lastIndexOf("/"));
|
||||
const unixTime = Math.floor(Date.now() / 1000);
|
||||
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;
|
||||
if (exitCode !== 0)
|
||||
return;
|
||||
@@ -252,7 +259,7 @@ Item {
|
||||
}
|
||||
|
||||
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
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
@@ -351,7 +358,7 @@ Item {
|
||||
}
|
||||
|
||||
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)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
|
||||
@@ -14,10 +14,10 @@ Singleton {
|
||||
|
||||
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
||||
readonly property string hyprDmsDir: configDir + "/hypr/dms"
|
||||
readonly property string outputsPath: hyprDmsDir + "/outputs.conf"
|
||||
readonly property string layoutPath: hyprDmsDir + "/layout.conf"
|
||||
readonly property string cursorPath: hyprDmsDir + "/cursor.conf"
|
||||
readonly property string windowrulesPath: hyprDmsDir + "/windowrules.conf"
|
||||
readonly property string outputsPath: hyprDmsDir + "/outputs.lua"
|
||||
readonly property string layoutPath: hyprDmsDir + "/layout.lua"
|
||||
readonly property string cursorPath: hyprDmsDir + "/cursor.lua"
|
||||
readonly property string windowrulesPath: hyprDmsDir + "/windowrules.lua"
|
||||
|
||||
property int _lastGapValue: -1
|
||||
|
||||
@@ -31,7 +31,7 @@ Singleton {
|
||||
function ensureWindowrulesConfig() {
|
||||
Proc.runCommand("hypr-ensure-windowrules", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && [ ! -f "${windowrulesPath}" ] && touch "${windowrulesPath}" || true`], (output, exitCode) => {
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!outputsData || Object.keys(outputsData).length === 0) {
|
||||
if (callback)
|
||||
@@ -70,8 +82,7 @@ Singleton {
|
||||
}
|
||||
|
||||
const settings = hyprlandSettings || SettingsData.hyprlandOutputSettings;
|
||||
let lines = ["# Auto-generated by DMS - do not edit manually", ""];
|
||||
let monitorv2Blocks = [];
|
||||
let lines = ["-- Auto-generated by DMS — do not edit manually", ""];
|
||||
|
||||
for (const outputName in outputsData) {
|
||||
const output = outputsData[outputName];
|
||||
@@ -82,7 +93,7 @@ Singleton {
|
||||
const outputSettings = settings[identifier] || {};
|
||||
|
||||
if (outputSettings.disabled) {
|
||||
lines.push("monitor = " + identifier + ", disable");
|
||||
lines.push(`hl.monitor({ output = ${luaQuoted(identifier)}, disabled = true })`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -98,68 +109,42 @@ Singleton {
|
||||
const position = x + "x" + y;
|
||||
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");
|
||||
if (transform !== 0)
|
||||
monitorLine += ", transform, " + transform;
|
||||
parts.push(`transform = ${transform}`);
|
||||
|
||||
if (output.vrr_supported) {
|
||||
const vrrMode = outputSettings.vrrFullscreenOnly ? 2 : (output.vrr_enabled ? 1 : 0);
|
||||
monitorLine += ", vrr, " + vrrMode;
|
||||
parts.push(`vrr = ${vrrMode}`);
|
||||
}
|
||||
|
||||
if (output.mirror && output.mirror.length > 0)
|
||||
monitorLine += ", mirror, " + output.mirror;
|
||||
parts.push(`mirror = ${luaQuoted(output.mirror)}`);
|
||||
|
||||
if (outputSettings.bitdepth && outputSettings.bitdepth !== 8)
|
||||
monitorLine += ", bitdepth, " + outputSettings.bitdepth;
|
||||
parts.push(`bitdepth = ${Number(outputSettings.bitdepth)}`);
|
||||
|
||||
if (outputSettings.colorManagement && outputSettings.colorManagement !== "auto")
|
||||
monitorLine += ", cm, " + outputSettings.colorManagement;
|
||||
parts.push(`cm = ${luaQuoted(outputSettings.colorManagement)}`);
|
||||
|
||||
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)
|
||||
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) {
|
||||
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("hl.monitor({ " + parts.join(", ") + " })");
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
|
||||
const content = lines.join("\n");
|
||||
|
||||
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 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 {
|
||||
gaps_in = ${gaps}
|
||||
gaps_out = ${gaps}
|
||||
border_size = ${borderSize}
|
||||
}
|
||||
|
||||
decoration {
|
||||
rounding = ${cornerRadius}
|
||||
}
|
||||
hl.config({
|
||||
general = {
|
||||
gaps_in = ${gaps},
|
||||
gaps_out = ${gaps},
|
||||
border_size = ${borderSize},
|
||||
},
|
||||
decoration = {
|
||||
rounding = ${cornerRadius},
|
||||
},
|
||||
})
|
||||
`;
|
||||
|
||||
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;
|
||||
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)
|
||||
log.warn("Failed to write cursor config:", output);
|
||||
});
|
||||
@@ -289,32 +275,34 @@ decoration {
|
||||
const hasCursorSettings = hideOnKeyPress || hideOnTouch || inactiveTimeout > 0;
|
||||
|
||||
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)
|
||||
log.warn("Failed to write cursor config:", output);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let lines = ["# Auto-generated by DMS - do not edit manually", ""];
|
||||
let lines = ["-- Auto-generated by DMS — do not edit manually", ""];
|
||||
|
||||
if (hasTheme) {
|
||||
lines.push(`env = HYPRCURSOR_THEME,${themeName}`);
|
||||
lines.push(`env = XCURSOR_THEME,${themeName}`);
|
||||
lines.push(`hl.env("HYPRCURSOR_THEME", ${luaQuoted(themeName)})`);
|
||||
lines.push(`hl.env("XCURSOR_THEME", ${luaQuoted(themeName)})`);
|
||||
}
|
||||
lines.push(`env = HYPRCURSOR_SIZE,${size}`);
|
||||
lines.push(`env = XCURSOR_SIZE,${size}`);
|
||||
lines.push(`hl.env("HYPRCURSOR_SIZE", ${luaQuoted(String(size))})`);
|
||||
lines.push(`hl.env("XCURSOR_SIZE", ${luaQuoted(String(size))})`);
|
||||
|
||||
if (hasCursorSettings) {
|
||||
lines.push("");
|
||||
lines.push("cursor {");
|
||||
lines.push("hl.config({");
|
||||
lines.push("\tcursor = {");
|
||||
if (hideOnKeyPress)
|
||||
lines.push(" hide_on_key_press = true");
|
||||
lines.push("\t\thide_on_key_press = true,");
|
||||
if (hideOnTouch)
|
||||
lines.push(" hide_on_touch = true");
|
||||
lines.push("\t\thide_on_touch = true,");
|
||||
if (inactiveTimeout > 0)
|
||||
lines.push(` inactive_timeout = ${inactiveTimeout}`);
|
||||
lines.push("}");
|
||||
lines.push(`\t\tinactive_timeout = ${inactiveTimeout},`);
|
||||
lines.push("\t},");
|
||||
lines.push("})");
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
|
||||
@@ -7,6 +7,7 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import "../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve
|
||||
import "../Common/KeybindActions.js" as Actions
|
||||
|
||||
Singleton {
|
||||
@@ -82,6 +83,7 @@ Singleton {
|
||||
case "niri":
|
||||
return compositorConfigDir + "/dms/binds.kdl";
|
||||
case "hyprland":
|
||||
return compositorConfigDir + "/dms/binds.lua";
|
||||
case "mangowc":
|
||||
return compositorConfigDir + "/dms/binds.conf";
|
||||
default:
|
||||
@@ -93,7 +95,7 @@ Singleton {
|
||||
case "niri":
|
||||
return compositorConfigDir + "/config.kdl";
|
||||
case "hyprland":
|
||||
return compositorConfigDir + "/hyprland.conf";
|
||||
return compositorConfigDir + "/hyprland.lua";
|
||||
case "mangowc":
|
||||
return compositorConfigDir + "/config.conf";
|
||||
default:
|
||||
@@ -247,8 +249,8 @@ Singleton {
|
||||
root.lastError = "";
|
||||
root.dmsBindsIncluded = true;
|
||||
root.dmsBindsFixed();
|
||||
const bindsFile = root.currentProvider === "niri" ? "dms/binds.kdl" : "dms/binds.conf";
|
||||
ToastService.showInfo(I18n.tr("Binds include added"), I18n.tr("%1 is now included in config").arg(bindsFile), "", "keybinds");
|
||||
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(bindsRel), "", "keybinds");
|
||||
Qt.callLater(root.forceReload);
|
||||
}
|
||||
}
|
||||
@@ -262,13 +264,36 @@ Singleton {
|
||||
let script;
|
||||
switch (currentProvider) {
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
default:
|
||||
fixing = false;
|
||||
@@ -321,6 +346,7 @@ Singleton {
|
||||
"statusMessage": status.statusMessage ?? ""
|
||||
};
|
||||
}
|
||||
_maybeWarnHyprlandLegacyConf();
|
||||
|
||||
if (!_rawData?.binds) {
|
||||
_allBinds = {};
|
||||
@@ -365,10 +391,13 @@ Singleton {
|
||||
for (var i = 0; i < binds.length; i++) {
|
||||
const bind = binds[i];
|
||||
const action = bind.action || "";
|
||||
const sourceStr = bind.source || "config";
|
||||
const keyData = {
|
||||
"key": bind.key || "",
|
||||
"source": bind.source || "config",
|
||||
"isOverride": bind.source === "dms",
|
||||
"source": sourceStr,
|
||||
"isOverride": sourceStr === "dms",
|
||||
"isDMSManaged": sourceStr === "dms" || sourceStr === "dms-default",
|
||||
"hasDefault": bind.hasDefault === true,
|
||||
"cooldownMs": bind.cooldownMs || 0,
|
||||
"flags": bind.flags || "",
|
||||
"allowWhenLocked": bind.allowWhenLocked || false,
|
||||
@@ -456,6 +485,19 @@ Singleton {
|
||||
_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) {
|
||||
if (!key)
|
||||
return;
|
||||
@@ -464,6 +506,14 @@ Singleton {
|
||||
bindRemoved(key);
|
||||
}
|
||||
|
||||
function resetBind(key) {
|
||||
if (!key)
|
||||
return;
|
||||
removeProcess.command = ["dms", "keybinds", "reset", currentProvider, key];
|
||||
removeProcess.running = true;
|
||||
bindRemoved(key);
|
||||
}
|
||||
|
||||
function isDmsAction(action) {
|
||||
return Actions.isDmsAction(action);
|
||||
}
|
||||
|
||||
@@ -66,13 +66,12 @@ Item {
|
||||
signal toggleExpand
|
||||
signal saveBind(string originalKey, var newData)
|
||||
signal removeBind(string key)
|
||||
signal resetBind(string key)
|
||||
signal cancelEdit
|
||||
|
||||
implicitHeight: contentColumn.implicitHeight
|
||||
height: implicitHeight
|
||||
|
||||
Component.onDestruction: _destroyShortcutInhibitor()
|
||||
|
||||
Component.onCompleted: {
|
||||
if (isNew && isExpanded)
|
||||
resetEdits();
|
||||
@@ -831,9 +830,12 @@ Item {
|
||||
color: root._actionType === modelData.id ? Theme.surfaceContainerHighest : Theme.surfaceContainer
|
||||
border.color: root._actionType === modelData.id ? Theme.outline : (typeArea.containsMouse ? Theme.outlineVariant : "transparent")
|
||||
border.width: 1
|
||||
clip: true
|
||||
|
||||
RowLayout {
|
||||
anchors.centerIn: parent
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
@@ -843,10 +845,13 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: typeDelegate.modelData.label
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: root._actionType === typeDelegate.modelData.id ? Theme.surfaceText : Theme.surfaceVariantText
|
||||
visible: typeDelegate.width > 100
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1763,10 +1768,19 @@ Item {
|
||||
iconName: "delete"
|
||||
iconSize: Theme.iconSize - 4
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[templates.dmshyprland]
|
||||
input_path = 'SHELL_DIR/matugen/templates/hypr-colors.conf'
|
||||
output_path = 'CONFIG_DIR/hypr/dms/colors.conf'
|
||||
input_path = 'SHELL_DIR/matugen/templates/hypr-colors.lua'
|
||||
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