mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
Compare commits
7 Commits
a205df1bd6
...
ccc7047be0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccc7047be0 | ||
|
|
a5e107c89d | ||
|
|
646d60dcbf | ||
|
|
5dc7c0d797 | ||
|
|
db1de9df38 | ||
|
|
3dd21382ba | ||
|
|
ec2b3d0d4b |
@@ -514,5 +514,6 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
matugenCmd,
|
matugenCmd,
|
||||||
clipboardCmd,
|
clipboardCmd,
|
||||||
doctorCmd,
|
doctorCmd,
|
||||||
|
configCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
318
core/cmd/dms/commands_config.go
Normal file
318
core/cmd/dms/commands_config.go
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configCmd = &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Configuration utilities",
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolveIncludeCmd = &cobra.Command{
|
||||||
|
Use: "resolve-include <compositor> <filename>",
|
||||||
|
Short: "Check if a file is included in compositor config",
|
||||||
|
Long: "Recursively check if a file is included/sourced in compositor configuration. Returns JSON with exists and included status.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
switch len(args) {
|
||||||
|
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 nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runResolveInclude,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
configCmd.AddCommand(resolveIncludeCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IncludeResult struct {
|
||||||
|
Exists bool `json:"exists"`
|
||||||
|
Included bool `json:"included"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func runResolveInclude(cmd *cobra.Command, args []string) {
|
||||||
|
compositor := strings.ToLower(args[0])
|
||||||
|
filename := args[1]
|
||||||
|
|
||||||
|
var result IncludeResult
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch compositor {
|
||||||
|
case "hyprland":
|
||||||
|
result, err = checkHyprlandInclude(filename)
|
||||||
|
case "niri":
|
||||||
|
result, err = checkNiriInclude(filename)
|
||||||
|
case "mangowc", "dwl", "mango":
|
||||||
|
result, err = checkMangoWCInclude(filename)
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown compositor: %s", compositor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error checking include: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := json.Marshal(result)
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkHyprlandInclude(filename string) (IncludeResult, error) {
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||||
|
if err != nil {
|
||||||
|
return IncludeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
|
result := IncludeResult{}
|
||||||
|
|
||||||
|
if _, err := os.Stat(targetPath); err == nil {
|
||||||
|
result.Exists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
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 {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if processed[absPath] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
processed[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDir := filepath.Dir(absPath)
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(trimmed, "source") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcePath := strings.TrimSpace(parts[1])
|
||||||
|
if matchesTarget(sourcePath, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := sourcePath
|
||||||
|
if !filepath.IsAbs(sourcePath) {
|
||||||
|
fullPath = filepath.Join(baseDir, sourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded, err := utils.ExpandPath(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if hyprlandFindInclude(expanded, target, processed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkNiriInclude(filename string) (IncludeResult, error) {
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||||
|
if err != nil {
|
||||||
|
return IncludeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
|
result := IncludeResult{}
|
||||||
|
|
||||||
|
if _, err := os.Stat(targetPath); err == nil {
|
||||||
|
result.Exists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(configDir, "config.kdl")
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
result.Included = niriFindInclude(mainConfig, "dms/"+filename, processed)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func niriFindInclude(filePath, target string, processed map[string]bool) bool {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if processed[absPath] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
processed[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDir := filepath.Dir(absPath)
|
||||||
|
content := string(data)
|
||||||
|
|
||||||
|
for _, line := range strings.Split(content, "\n") {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "//") || trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(trimmed, "include") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
startQuote := strings.Index(trimmed, "\"")
|
||||||
|
if startQuote == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
endQuote := strings.LastIndex(trimmed, "\"")
|
||||||
|
if endQuote <= startQuote {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
includePath := trimmed[startQuote+1 : endQuote]
|
||||||
|
if matchesTarget(includePath, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := includePath
|
||||||
|
if !filepath.IsAbs(includePath) {
|
||||||
|
fullPath = filepath.Join(baseDir, includePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if niriFindInclude(fullPath, target, processed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMangoWCInclude(filename string) (IncludeResult, error) {
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/mango")
|
||||||
|
if err != nil {
|
||||||
|
return IncludeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
|
result := IncludeResult{}
|
||||||
|
|
||||||
|
if _, err := os.Stat(targetPath); err == nil {
|
||||||
|
result.Exists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(configDir, "config.conf")
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
mainConfig = filepath.Join(configDir, "mango.conf")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
result.Included = mangowcFindInclude(mainConfig, "dms/"+filename, processed)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mangowcFindInclude(filePath, target string, processed map[string]bool) bool {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if processed[absPath] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
processed[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDir := filepath.Dir(absPath)
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(trimmed, "source") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcePath := strings.TrimSpace(parts[1])
|
||||||
|
if matchesTarget(sourcePath, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := sourcePath
|
||||||
|
if !filepath.IsAbs(sourcePath) {
|
||||||
|
fullPath = filepath.Join(baseDir, sourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded, err := utils.ExpandPath(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if mangowcFindInclude(expanded, target, processed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesTarget(path, target string) bool {
|
||||||
|
path = strings.TrimPrefix(path, "./")
|
||||||
|
target = strings.TrimPrefix(target, "./")
|
||||||
|
return path == target || strings.HasSuffix(path, "/"+target)
|
||||||
|
}
|
||||||
@@ -64,6 +64,7 @@ func init() {
|
|||||||
keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds")
|
keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds")
|
||||||
keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat")
|
keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat")
|
||||||
keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)")
|
keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)")
|
||||||
|
keybindsSetCmd.Flags().String("flags", "", "Hyprland bind flags (e.g., 'e' for repeat, 'l' for locked, 'r' for release)")
|
||||||
|
|
||||||
keybindsCmd.AddCommand(keybindsListCmd)
|
keybindsCmd.AddCommand(keybindsListCmd)
|
||||||
keybindsCmd.AddCommand(keybindsShowCmd)
|
keybindsCmd.AddCommand(keybindsShowCmd)
|
||||||
@@ -211,6 +212,9 @@ func runKeybindsSet(cmd *cobra.Command, args []string) {
|
|||||||
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
||||||
options["repeat"] = false
|
options["repeat"] = false
|
||||||
}
|
}
|
||||||
|
if v, _ := cmd.Flags().GetString("flags"); v != "" {
|
||||||
|
options["flags"] = v
|
||||||
|
}
|
||||||
|
|
||||||
desc, _ := cmd.Flags().GetString("desc")
|
desc, _ := cmd.Flags().GetString("desc")
|
||||||
if err := writable.SetBind(key, action, desc, options); err != nil {
|
if err := writable.SetBind(key, action, desc, options); err != nil {
|
||||||
|
|||||||
@@ -543,7 +543,7 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cd.deployHyprlandDmsConfigs(dmsDir); err != nil {
|
if err := cd.deployHyprlandDmsConfigs(dmsDir, terminalCommand); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -553,13 +553,14 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string) error {
|
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalCommand string) error {
|
||||||
configs := []struct {
|
configs := []struct {
|
||||||
name string
|
name string
|
||||||
content string
|
content string
|
||||||
}{
|
}{
|
||||||
{"colors.conf", HyprColorsConfig},
|
{"colors.conf", HyprColorsConfig},
|
||||||
{"layout.conf", HyprLayoutConfig},
|
{"layout.conf", HyprLayoutConfig},
|
||||||
|
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||||
{"outputs.conf", ""},
|
{"outputs.conf", ""},
|
||||||
{"cursor.conf", ""},
|
{"cursor.conf", ""},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -408,7 +408,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
|
|||||||
content, err := os.ReadFile(result.Path)
|
content, err := os.ReadFile(result.Path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
||||||
assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty")
|
assert.Contains(t, string(content), "source = ./dms/binds.conf")
|
||||||
assert.Contains(t, string(content), "exec-once = ")
|
assert.Contains(t, string(content), "exec-once = ")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -444,7 +444,7 @@ general {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
|
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), "monitor = HDMI-A-1, 3840x2160@60")
|
||||||
assert.Contains(t, string(newContent), "bind = $mod, T, exec, kitty")
|
assert.Contains(t, string(newContent), "source = ./dms/binds.conf")
|
||||||
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -461,9 +461,7 @@ func TestHyprlandConfigStructure(t *testing.T) {
|
|||||||
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
||||||
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
||||||
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
||||||
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
|
assert.Contains(t, HyprlandConfig, "source = ./dms/binds.conf")
|
||||||
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, {{TERMINAL_COMMAND}}")
|
|
||||||
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGhosttyConfigStructure(t *testing.T) {
|
func TestGhosttyConfigStructure(t *testing.T) {
|
||||||
|
|||||||
155
core/internal/config/embedded/hypr-binds.conf
Normal file
155
core/internal/config/embedded/hypr-binds.conf
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# === 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
|
||||||
|
|
||||||
|
# === 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
|
||||||
|
|
||||||
|
# === 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
|
||||||
|
|
||||||
|
# === 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
|
||||||
|
|
||||||
|
# === 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%
|
||||||
|
|
||||||
|
# === 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
|
||||||
|
|
||||||
|
# === System Controls ===
|
||||||
|
bind = SUPER SHIFT, P, dpms, toggle
|
||||||
@@ -111,168 +111,8 @@ windowrule = float on, match:class ^(zoom)$
|
|||||||
|
|
||||||
layerrule = no_anim on, match:namespace ^(quickshell)$
|
layerrule = no_anim on, match:namespace ^(quickshell)$
|
||||||
|
|
||||||
# ==================
|
|
||||||
# KEYBINDINGS
|
|
||||||
# ==================
|
|
||||||
$mod = SUPER
|
|
||||||
|
|
||||||
# === Application Launchers ===
|
|
||||||
bind = $mod, T, exec, {{TERMINAL_COMMAND}}
|
|
||||||
bind = $mod, space, exec, dms ipc call spotlight toggle
|
|
||||||
bind = $mod, V, exec, dms ipc call clipboard toggle
|
|
||||||
bind = $mod, M, exec, dms ipc call processlist focusOrToggle
|
|
||||||
bind = $mod, comma, exec, dms ipc call settings focusOrToggle
|
|
||||||
bind = $mod, N, exec, dms ipc call notifications toggle
|
|
||||||
bind = $mod SHIFT, N, exec, dms ipc call notepad toggle
|
|
||||||
bind = $mod, Y, exec, dms ipc call dankdash wallpaper
|
|
||||||
bind = $mod, TAB, exec, dms ipc call hypr toggleOverview
|
|
||||||
|
|
||||||
# === Cheat sheet
|
|
||||||
bind = $mod SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
|
|
||||||
|
|
||||||
# === Security ===
|
|
||||||
bind = $mod ALT, L, exec, dms ipc call lock lock
|
|
||||||
bind = $mod 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
|
|
||||||
|
|
||||||
# === Brightness Controls ===
|
|
||||||
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
|
||||||
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
|
|
||||||
|
|
||||||
# === Window Management ===
|
|
||||||
bind = $mod, Q, killactive
|
|
||||||
bind = $mod, F, fullscreen, 1
|
|
||||||
bind = $mod SHIFT, F, fullscreen, 0
|
|
||||||
bind = $mod SHIFT, T, togglefloating
|
|
||||||
bind = $mod, W, togglegroup
|
|
||||||
|
|
||||||
# === Focus Navigation ===
|
|
||||||
bind = $mod, left, movefocus, l
|
|
||||||
bind = $mod, down, movefocus, d
|
|
||||||
bind = $mod, up, movefocus, u
|
|
||||||
bind = $mod, right, movefocus, r
|
|
||||||
bind = $mod, H, movefocus, l
|
|
||||||
bind = $mod, J, movefocus, d
|
|
||||||
bind = $mod, K, movefocus, u
|
|
||||||
bind = $mod, L, movefocus, r
|
|
||||||
|
|
||||||
# === Window Movement ===
|
|
||||||
bind = $mod SHIFT, left, movewindow, l
|
|
||||||
bind = $mod SHIFT, down, movewindow, d
|
|
||||||
bind = $mod SHIFT, up, movewindow, u
|
|
||||||
bind = $mod SHIFT, right, movewindow, r
|
|
||||||
bind = $mod SHIFT, H, movewindow, l
|
|
||||||
bind = $mod SHIFT, J, movewindow, d
|
|
||||||
bind = $mod SHIFT, K, movewindow, u
|
|
||||||
bind = $mod SHIFT, L, movewindow, r
|
|
||||||
|
|
||||||
# === Column Navigation ===
|
|
||||||
bind = $mod, Home, focuswindow, first
|
|
||||||
bind = $mod, End, focuswindow, last
|
|
||||||
|
|
||||||
# === Monitor Navigation ===
|
|
||||||
bind = $mod CTRL, left, focusmonitor, l
|
|
||||||
bind = $mod CTRL, right, focusmonitor, r
|
|
||||||
bind = $mod CTRL, H, focusmonitor, l
|
|
||||||
bind = $mod CTRL, J, focusmonitor, d
|
|
||||||
bind = $mod CTRL, K, focusmonitor, u
|
|
||||||
bind = $mod CTRL, L, focusmonitor, r
|
|
||||||
|
|
||||||
# === Move to Monitor ===
|
|
||||||
bind = $mod SHIFT CTRL, left, movewindow, mon:l
|
|
||||||
bind = $mod SHIFT CTRL, down, movewindow, mon:d
|
|
||||||
bind = $mod SHIFT CTRL, up, movewindow, mon:u
|
|
||||||
bind = $mod SHIFT CTRL, right, movewindow, mon:r
|
|
||||||
bind = $mod SHIFT CTRL, H, movewindow, mon:l
|
|
||||||
bind = $mod SHIFT CTRL, J, movewindow, mon:d
|
|
||||||
bind = $mod SHIFT CTRL, K, movewindow, mon:u
|
|
||||||
bind = $mod SHIFT CTRL, L, movewindow, mon:r
|
|
||||||
|
|
||||||
# === Workspace Navigation ===
|
|
||||||
bind = $mod, Page_Down, workspace, e+1
|
|
||||||
bind = $mod, Page_Up, workspace, e-1
|
|
||||||
bind = $mod, U, workspace, e+1
|
|
||||||
bind = $mod, I, workspace, e-1
|
|
||||||
bind = $mod CTRL, down, movetoworkspace, e+1
|
|
||||||
bind = $mod CTRL, up, movetoworkspace, e-1
|
|
||||||
bind = $mod CTRL, U, movetoworkspace, e+1
|
|
||||||
bind = $mod CTRL, I, movetoworkspace, e-1
|
|
||||||
|
|
||||||
# === Move Workspaces ===
|
|
||||||
bind = $mod SHIFT, Page_Down, movetoworkspace, e+1
|
|
||||||
bind = $mod SHIFT, Page_Up, movetoworkspace, e-1
|
|
||||||
bind = $mod SHIFT, U, movetoworkspace, e+1
|
|
||||||
bind = $mod SHIFT, I, movetoworkspace, e-1
|
|
||||||
|
|
||||||
# === Mouse Wheel Navigation ===
|
|
||||||
bind = $mod, mouse_down, workspace, e+1
|
|
||||||
bind = $mod, mouse_up, workspace, e-1
|
|
||||||
bind = $mod CTRL, mouse_down, movetoworkspace, e+1
|
|
||||||
bind = $mod CTRL, mouse_up, movetoworkspace, e-1
|
|
||||||
|
|
||||||
# === Numbered Workspaces ===
|
|
||||||
bind = $mod, 1, workspace, 1
|
|
||||||
bind = $mod, 2, workspace, 2
|
|
||||||
bind = $mod, 3, workspace, 3
|
|
||||||
bind = $mod, 4, workspace, 4
|
|
||||||
bind = $mod, 5, workspace, 5
|
|
||||||
bind = $mod, 6, workspace, 6
|
|
||||||
bind = $mod, 7, workspace, 7
|
|
||||||
bind = $mod, 8, workspace, 8
|
|
||||||
bind = $mod, 9, workspace, 9
|
|
||||||
|
|
||||||
# === Move to Numbered Workspaces ===
|
|
||||||
bind = $mod SHIFT, 1, movetoworkspace, 1
|
|
||||||
bind = $mod SHIFT, 2, movetoworkspace, 2
|
|
||||||
bind = $mod SHIFT, 3, movetoworkspace, 3
|
|
||||||
bind = $mod SHIFT, 4, movetoworkspace, 4
|
|
||||||
bind = $mod SHIFT, 5, movetoworkspace, 5
|
|
||||||
bind = $mod SHIFT, 6, movetoworkspace, 6
|
|
||||||
bind = $mod SHIFT, 7, movetoworkspace, 7
|
|
||||||
bind = $mod SHIFT, 8, movetoworkspace, 8
|
|
||||||
bind = $mod SHIFT, 9, movetoworkspace, 9
|
|
||||||
|
|
||||||
# === Column Management ===
|
|
||||||
bind = $mod, bracketleft, layoutmsg, preselect l
|
|
||||||
bind = $mod, bracketright, layoutmsg, preselect r
|
|
||||||
|
|
||||||
# === Sizing & Layout ===
|
|
||||||
bind = $mod, R, layoutmsg, togglesplit
|
|
||||||
bind = $mod CTRL, F, resizeactive, exact 100%
|
|
||||||
|
|
||||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
|
||||||
bindmd = $mod, mouse:272, Move window, movewindow
|
|
||||||
bindmd = $mod, mouse:273, Resize window, resizewindow
|
|
||||||
|
|
||||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
|
||||||
bindd = $mod, code:20, Expand window left, resizeactive, -100 0
|
|
||||||
bindd = $mod, code:21, Shrink window left, resizeactive, 100 0
|
|
||||||
|
|
||||||
# === Manual Sizing ===
|
|
||||||
binde = $mod, minus, resizeactive, -10% 0
|
|
||||||
binde = $mod, equal, resizeactive, 10% 0
|
|
||||||
binde = $mod SHIFT, minus, resizeactive, 0 -10%
|
|
||||||
binde = $mod 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
|
|
||||||
|
|
||||||
# === System Controls ===
|
|
||||||
bind = $mod SHIFT, P, dpms, toggle
|
|
||||||
|
|
||||||
source = ./dms/colors.conf
|
source = ./dms/colors.conf
|
||||||
source = ./dms/outputs.conf
|
source = ./dms/outputs.conf
|
||||||
source = ./dms/layout.conf
|
source = ./dms/layout.conf
|
||||||
source = ./dms/cursor.conf
|
source = ./dms/cursor.conf
|
||||||
|
source = ./dms/binds.conf
|
||||||
|
|||||||
@@ -10,3 +10,6 @@ var HyprColorsConfig string
|
|||||||
|
|
||||||
//go:embed embedded/hypr-layout.conf
|
//go:embed embedded/hypr-layout.conf
|
||||||
var HyprLayoutConfig string
|
var HyprLayoutConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/hypr-binds.conf
|
||||||
|
var HyprBindsConfig string
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory st
|
|||||||
Action: rawAction,
|
Action: rawAction,
|
||||||
Subcategory: subcategory,
|
Subcategory: subcategory,
|
||||||
Source: source,
|
Source: source,
|
||||||
|
Flags: kb.Flags,
|
||||||
}
|
}
|
||||||
|
|
||||||
if source == "dms" && conflicts != nil {
|
if source == "dms" && conflicts != nil {
|
||||||
@@ -224,11 +225,20 @@ func (h *HyprlandProvider) SetBind(key, action, description string, options map[
|
|||||||
existingBinds = make(map[string]*hyprlandOverrideBind)
|
existingBinds = make(map[string]*hyprlandOverrideBind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract flags from options
|
||||||
|
var flags string
|
||||||
|
if options != nil {
|
||||||
|
if f, ok := options["flags"].(string); ok {
|
||||||
|
flags = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
normalizedKey := strings.ToLower(key)
|
normalizedKey := strings.ToLower(key)
|
||||||
existingBinds[normalizedKey] = &hyprlandOverrideBind{
|
existingBinds[normalizedKey] = &hyprlandOverrideBind{
|
||||||
Key: key,
|
Key: key,
|
||||||
Action: action,
|
Action: action,
|
||||||
Description: description,
|
Description: description,
|
||||||
|
Flags: flags,
|
||||||
Options: options,
|
Options: options,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +260,7 @@ type hyprlandOverrideBind struct {
|
|||||||
Key string
|
Key string
|
||||||
Action string
|
Action string
|
||||||
Description string
|
Description string
|
||||||
|
Flags string // Bind flags: l=locked, r=release, e=repeat, n=non-consuming, m=mouse, t=transparent, i=ignore-mods, s=separate, d=description, o=long-press
|
||||||
Options map[string]any
|
Options map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,6 +292,11 @@ func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract flags from bind type
|
||||||
|
bindType := strings.TrimSpace(parts[0])
|
||||||
|
flags := extractBindFlags(bindType)
|
||||||
|
hasDescFlag := strings.Contains(flags, "d")
|
||||||
|
|
||||||
content := strings.TrimSpace(parts[1])
|
content := strings.TrimSpace(parts[1])
|
||||||
commentParts := strings.SplitN(content, "#", 2)
|
commentParts := strings.SplitN(content, "#", 2)
|
||||||
bindContent := strings.TrimSpace(commentParts[0])
|
bindContent := strings.TrimSpace(commentParts[0])
|
||||||
@@ -290,18 +306,41 @@ func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind
|
|||||||
comment = strings.TrimSpace(commentParts[1])
|
comment = strings.TrimSpace(commentParts[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := strings.SplitN(bindContent, ",", 4)
|
// For bindd, format is: mods, key, description, dispatcher, params
|
||||||
if len(fields) < 3 {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
mods := strings.TrimSpace(fields[0])
|
mods := strings.TrimSpace(fields[0])
|
||||||
keyName := strings.TrimSpace(fields[1])
|
keyName := strings.TrimSpace(fields[1])
|
||||||
dispatcher := strings.TrimSpace(fields[2])
|
|
||||||
|
|
||||||
var params string
|
var dispatcher, params string
|
||||||
if len(fields) > 3 {
|
if hasDescFlag {
|
||||||
params = strings.TrimSpace(fields[3])
|
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)
|
keyStr := h.buildKeyString(mods, keyName)
|
||||||
@@ -315,6 +354,7 @@ func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind
|
|||||||
Key: keyStr,
|
Key: keyStr,
|
||||||
Action: action,
|
Action: action,
|
||||||
Description: comment,
|
Description: comment,
|
||||||
|
Flags: flags,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,11 +431,23 @@ func (h *HyprlandProvider) writeBindLine(sb *strings.Builder, bind *hyprlandOver
|
|||||||
mods, key := h.parseKeyString(bind.Key)
|
mods, key := h.parseKeyString(bind.Key)
|
||||||
dispatcher, params := h.parseAction(bind.Action)
|
dispatcher, params := h.parseAction(bind.Action)
|
||||||
|
|
||||||
sb.WriteString("bind = ")
|
// Write bind type with flags (e.g., "bind", "binde", "bindel")
|
||||||
|
sb.WriteString("bind")
|
||||||
|
if bind.Flags != "" {
|
||||||
|
sb.WriteString(bind.Flags)
|
||||||
|
}
|
||||||
|
sb.WriteString(" = ")
|
||||||
sb.WriteString(mods)
|
sb.WriteString(mods)
|
||||||
sb.WriteString(", ")
|
sb.WriteString(", ")
|
||||||
sb.WriteString(key)
|
sb.WriteString(key)
|
||||||
sb.WriteString(", ")
|
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)
|
sb.WriteString(dispatcher)
|
||||||
|
|
||||||
if params != "" {
|
if params != "" {
|
||||||
@@ -403,7 +455,8 @@ func (h *HyprlandProvider) writeBindLine(sb *strings.Builder, bind *hyprlandOver
|
|||||||
sb.WriteString(params)
|
sb.WriteString(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
if bind.Description != "" {
|
// Only add comment if not using bindd (which has inline description)
|
||||||
|
if bind.Description != "" && !strings.Contains(bind.Flags, "d") {
|
||||||
sb.WriteString(" # ")
|
sb.WriteString(" # ")
|
||||||
sb.WriteString(bind.Description)
|
sb.WriteString(bind.Description)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type HyprlandKeyBinding struct {
|
|||||||
Params string `json:"params"`
|
Params string `json:"params"`
|
||||||
Comment string `json:"comment"`
|
Comment string `json:"comment"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
|
Flags string `json:"flags"` // 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
|
||||||
}
|
}
|
||||||
|
|
||||||
type HyprlandSection struct {
|
type HyprlandSection struct {
|
||||||
@@ -218,71 +219,7 @@ func hyprlandAutogenerateComment(dispatcher, params string) string {
|
|||||||
|
|
||||||
func (p *HyprlandParser) getKeybindAtLine(lineNumber int) *HyprlandKeyBinding {
|
func (p *HyprlandParser) getKeybindAtLine(lineNumber int) *HyprlandKeyBinding {
|
||||||
line := p.contentLines[lineNumber]
|
line := p.contentLines[lineNumber]
|
||||||
parts := strings.SplitN(line, "=", 2)
|
return p.parseBindLine(line)
|
||||||
if len(parts) < 2 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
keys := parts[1]
|
|
||||||
keyParts := strings.SplitN(keys, "#", 2)
|
|
||||||
keys = keyParts[0]
|
|
||||||
|
|
||||||
var comment string
|
|
||||||
if len(keyParts) > 1 {
|
|
||||||
comment = strings.TrimSpace(keyParts[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
keyFields := strings.SplitN(keys, ",", 5)
|
|
||||||
if len(keyFields) < 3 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mods := strings.TrimSpace(keyFields[0])
|
|
||||||
key := strings.TrimSpace(keyFields[1])
|
|
||||||
dispatcher := strings.TrimSpace(keyFields[2])
|
|
||||||
|
|
||||||
var params string
|
|
||||||
if len(keyFields) > 3 {
|
|
||||||
paramParts := keyFields[3:]
|
|
||||||
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
|
||||||
}
|
|
||||||
|
|
||||||
if comment != "" {
|
|
||||||
if strings.HasPrefix(comment, HideComment) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
comment = hyprlandAutogenerateComment(dispatcher, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
var modList []string
|
|
||||||
if mods != "" {
|
|
||||||
modstring := mods + string(ModSeparators[0])
|
|
||||||
p := 0
|
|
||||||
for index, char := range modstring {
|
|
||||||
isModSep := false
|
|
||||||
for _, sep := range ModSeparators {
|
|
||||||
if char == sep {
|
|
||||||
isModSep = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if isModSep {
|
|
||||||
if index-p > 1 {
|
|
||||||
modList = append(modList, modstring[p:index])
|
|
||||||
}
|
|
||||||
p = index + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &HyprlandKeyBinding{
|
|
||||||
Mods: modList,
|
|
||||||
Key: key,
|
|
||||||
Dispatcher: dispatcher,
|
|
||||||
Params: params,
|
|
||||||
Comment: comment,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *HyprlandParser) getBindsRecursive(currentContent *HyprlandSection, scope int) *HyprlandSection {
|
func (p *HyprlandParser) getBindsRecursive(currentContent *HyprlandSection, scope int) *HyprlandSection {
|
||||||
@@ -572,6 +509,11 @@ func (p *HyprlandParser) parseBindLine(line string) *HyprlandKeyBinding {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract bind type and flags from the left side of "="
|
||||||
|
bindType := strings.TrimSpace(parts[0])
|
||||||
|
flags := extractBindFlags(bindType)
|
||||||
|
hasDescFlag := strings.Contains(flags, "d")
|
||||||
|
|
||||||
keys := parts[1]
|
keys := parts[1]
|
||||||
keyParts := strings.SplitN(keys, "#", 2)
|
keyParts := strings.SplitN(keys, "#", 2)
|
||||||
keys = keyParts[0]
|
keys = keyParts[0]
|
||||||
@@ -581,19 +523,43 @@ func (p *HyprlandParser) parseBindLine(line string) *HyprlandKeyBinding {
|
|||||||
comment = strings.TrimSpace(keyParts[1])
|
comment = strings.TrimSpace(keyParts[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
keyFields := strings.SplitN(keys, ",", 5)
|
// For bindd, the format is: bindd = MODS, key, description, dispatcher, params
|
||||||
if len(keyFields) < 3 {
|
// For regular binds: bind = MODS, key, dispatcher, params
|
||||||
|
var minFields, descIndex, dispatcherIndex int
|
||||||
|
if hasDescFlag {
|
||||||
|
minFields = 4 // mods, key, description, dispatcher
|
||||||
|
descIndex = 2
|
||||||
|
dispatcherIndex = 3
|
||||||
|
} else {
|
||||||
|
minFields = 3 // mods, key, dispatcher
|
||||||
|
dispatcherIndex = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
keyFields := strings.SplitN(keys, ",", minFields+2) // Allow for params
|
||||||
|
if len(keyFields) < minFields {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
mods := strings.TrimSpace(keyFields[0])
|
mods := strings.TrimSpace(keyFields[0])
|
||||||
key := strings.TrimSpace(keyFields[1])
|
key := strings.TrimSpace(keyFields[1])
|
||||||
dispatcher := strings.TrimSpace(keyFields[2])
|
|
||||||
|
|
||||||
var params string
|
var dispatcher, params string
|
||||||
if len(keyFields) > 3 {
|
if hasDescFlag {
|
||||||
paramParts := keyFields[3:]
|
// bindd format: description is in the bind itself
|
||||||
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
if comment == "" {
|
||||||
|
comment = strings.TrimSpace(keyFields[descIndex])
|
||||||
|
}
|
||||||
|
dispatcher = strings.TrimSpace(keyFields[dispatcherIndex])
|
||||||
|
if len(keyFields) > dispatcherIndex+1 {
|
||||||
|
paramParts := keyFields[dispatcherIndex+1:]
|
||||||
|
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatcher = strings.TrimSpace(keyFields[dispatcherIndex])
|
||||||
|
if len(keyFields) > dispatcherIndex+1 {
|
||||||
|
paramParts := keyFields[dispatcherIndex+1:]
|
||||||
|
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if comment != "" && strings.HasPrefix(comment, HideComment) {
|
if comment != "" && strings.HasPrefix(comment, HideComment) {
|
||||||
@@ -631,9 +597,20 @@ func (p *HyprlandParser) parseBindLine(line string) *HyprlandKeyBinding {
|
|||||||
Dispatcher: dispatcher,
|
Dispatcher: dispatcher,
|
||||||
Params: params,
|
Params: params,
|
||||||
Comment: comment,
|
Comment: comment,
|
||||||
|
Flags: flags,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractBindFlags extracts the flags from a bind type string
|
||||||
|
// e.g., "binde" -> "e", "bindel" -> "el", "bindd" -> "d"
|
||||||
|
func extractBindFlags(bindType string) string {
|
||||||
|
bindType = strings.TrimSpace(bindType)
|
||||||
|
if !strings.HasPrefix(bindType, "bind") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return bindType[4:] // Everything after "bind"
|
||||||
|
}
|
||||||
|
|
||||||
func ParseHyprlandKeysWithDMS(path string) (*HyprlandParseResult, error) {
|
func ParseHyprlandKeysWithDMS(path string) (*HyprlandParseResult, error) {
|
||||||
parser := NewHyprlandParser(path)
|
parser := NewHyprlandParser(path)
|
||||||
section, err := parser.ParseWithDMS()
|
section, err := parser.ParseWithDMS()
|
||||||
|
|||||||
@@ -394,3 +394,126 @@ bind = SUPER, T, exec, kitty
|
|||||||
t.Errorf("Expected 2 keybinds (comments ignored), got %d", len(section.Keybinds))
|
t.Errorf("Expected 2 keybinds (comments ignored), got %d", len(section.Keybinds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtractBindFlags(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
bindType string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"bind", ""},
|
||||||
|
{"binde", "e"},
|
||||||
|
{"bindl", "l"},
|
||||||
|
{"bindr", "r"},
|
||||||
|
{"bindd", "d"},
|
||||||
|
{"bindo", "o"},
|
||||||
|
{"bindel", "el"},
|
||||||
|
{"bindler", "ler"},
|
||||||
|
{"bindem", "em"},
|
||||||
|
{" bind ", ""},
|
||||||
|
{" binde ", "e"},
|
||||||
|
{"notbind", ""},
|
||||||
|
{"", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.bindType, func(t *testing.T) {
|
||||||
|
result := extractBindFlags(tt.bindType)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("extractBindFlags(%q) = %q, want %q", tt.bindType, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandBindFlags(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
line string
|
||||||
|
expectedFlags string
|
||||||
|
expectedKey string
|
||||||
|
expectedDisp string
|
||||||
|
expectedDesc string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "regular bind",
|
||||||
|
line: "bind = SUPER, Q, killactive",
|
||||||
|
expectedFlags: "",
|
||||||
|
expectedKey: "Q",
|
||||||
|
expectedDisp: "killactive",
|
||||||
|
expectedDesc: "Close window",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binde (repeat on hold)",
|
||||||
|
line: "binde = , XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+",
|
||||||
|
expectedFlags: "e",
|
||||||
|
expectedKey: "XF86AudioRaiseVolume",
|
||||||
|
expectedDisp: "exec",
|
||||||
|
expectedDesc: "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bindl (locked/inhibitor bypass)",
|
||||||
|
line: "bindl = , XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-",
|
||||||
|
expectedFlags: "l",
|
||||||
|
expectedKey: "XF86AudioLowerVolume",
|
||||||
|
expectedDisp: "exec",
|
||||||
|
expectedDesc: "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bindr (release trigger)",
|
||||||
|
line: "bindr = SUPER, SUPER_L, exec, pkill wofi || wofi",
|
||||||
|
expectedFlags: "r",
|
||||||
|
expectedKey: "SUPER_L",
|
||||||
|
expectedDisp: "exec",
|
||||||
|
expectedDesc: "pkill wofi || wofi",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bindd (description)",
|
||||||
|
line: "bindd = SUPER, Q, Open my favourite terminal, exec, kitty",
|
||||||
|
expectedFlags: "d",
|
||||||
|
expectedKey: "Q",
|
||||||
|
expectedDisp: "exec",
|
||||||
|
expectedDesc: "Open my favourite terminal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bindo (long press)",
|
||||||
|
line: "bindo = SUPER, XF86AudioNext, exec, playerctl next",
|
||||||
|
expectedFlags: "o",
|
||||||
|
expectedKey: "XF86AudioNext",
|
||||||
|
expectedDisp: "exec",
|
||||||
|
expectedDesc: "playerctl next",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bindel (combined flags)",
|
||||||
|
line: "bindel = , XF86AudioRaiseVolume, exec, wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 5%+",
|
||||||
|
expectedFlags: "el",
|
||||||
|
expectedKey: "XF86AudioRaiseVolume",
|
||||||
|
expectedDisp: "exec",
|
||||||
|
expectedDesc: "wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 5%+",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
parser := NewHyprlandParser("")
|
||||||
|
parser.contentLines = []string{tt.line}
|
||||||
|
result := parser.getKeybindAtLine(0)
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("Expected keybind, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Flags != tt.expectedFlags {
|
||||||
|
t.Errorf("Flags = %q, want %q", result.Flags, tt.expectedFlags)
|
||||||
|
}
|
||||||
|
if result.Key != tt.expectedKey {
|
||||||
|
t.Errorf("Key = %q, want %q", result.Key, tt.expectedKey)
|
||||||
|
}
|
||||||
|
if result.Dispatcher != tt.expectedDisp {
|
||||||
|
t.Errorf("Dispatcher = %q, want %q", result.Dispatcher, tt.expectedDisp)
|
||||||
|
}
|
||||||
|
if result.Comment != tt.expectedDesc {
|
||||||
|
t.Errorf("Comment = %q, want %q", result.Comment, tt.expectedDesc)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -187,7 +187,15 @@ func (n *NiriProvider) formatRawAction(action string, args []string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return action + " " + strings.Join(args, " ")
|
quotedArgs := make([]string, len(args))
|
||||||
|
for i, arg := range args {
|
||||||
|
if arg == "" {
|
||||||
|
quotedArgs[i] = `""`
|
||||||
|
} else {
|
||||||
|
quotedArgs[i] = arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return action + " " + strings.Join(quotedArgs, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) formatKey(kb *NiriKeyBinding) string {
|
func (n *NiriProvider) formatKey(kb *NiriKeyBinding) string {
|
||||||
@@ -293,9 +301,15 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
keyStr := parser.formatBindKey(kb)
|
keyStr := parser.formatBindKey(kb)
|
||||||
|
|
||||||
|
action := n.buildActionFromNode(child)
|
||||||
|
if action == "" {
|
||||||
|
action = n.formatRawAction(kb.Action, kb.Args)
|
||||||
|
}
|
||||||
|
|
||||||
binds[keyStr] = &overrideBind{
|
binds[keyStr] = &overrideBind{
|
||||||
Key: keyStr,
|
Key: keyStr,
|
||||||
Action: n.formatRawAction(kb.Action, kb.Args),
|
Action: action,
|
||||||
Description: kb.Description,
|
Description: kb.Description,
|
||||||
Options: n.extractOptions(child),
|
Options: n.extractOptions(child),
|
||||||
}
|
}
|
||||||
@@ -305,6 +319,36 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
|
|||||||
return binds, nil
|
return binds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) buildActionFromNode(bindNode *document.Node) string {
|
||||||
|
if len(bindNode.Children) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
actionNode := bindNode.Children[0]
|
||||||
|
|
||||||
|
kdlStr := strings.TrimSpace(actionNode.String())
|
||||||
|
if kdlStr == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.kdlActionToInternal(kdlStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) kdlActionToInternal(kdlAction string) string {
|
||||||
|
parts := n.parseActionParts(kdlAction)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return kdlAction
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, part := range parts {
|
||||||
|
if part == "" {
|
||||||
|
parts[i] = `""`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) extractOptions(node *document.Node) map[string]any {
|
func (n *NiriProvider) extractOptions(node *document.Node) map[string]any {
|
||||||
if node.Properties == nil {
|
if node.Properties == nil {
|
||||||
return make(map[string]any)
|
return make(map[string]any)
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ func TestNiriFormatRawAction(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"spawn", []string{"kitty"}, "spawn kitty"},
|
{"spawn", []string{"kitty"}, "spawn kitty"},
|
||||||
{"spawn", []string{"dms", "ipc", "call"}, "spawn dms ipc call"},
|
{"spawn", []string{"dms", "ipc", "call"}, "spawn dms ipc call"},
|
||||||
|
{"spawn", []string{"dms", "ipc", "call", "brightness", "increment", "5", ""}, `spawn dms ipc call brightness increment 5 ""`},
|
||||||
|
{"spawn", []string{"dms", "ipc", "call", "dash", "toggle", ""}, `spawn dms ipc call dash toggle ""`},
|
||||||
{"close-window", nil, "close-window"},
|
{"close-window", nil, "close-window"},
|
||||||
{"fullscreen-window", nil, "fullscreen-window"},
|
{"fullscreen-window", nil, "fullscreen-window"},
|
||||||
{"focus-workspace", []string{"1"}, "focus-workspace 1"},
|
{"focus-workspace", []string{"1"}, "focus-workspace 1"},
|
||||||
@@ -324,6 +326,58 @@ func TestNiriGenerateBindsContentRoundTrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNiriEmptyArgsPreservation(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
binds := map[string]*overrideBind{
|
||||||
|
"XF86MonBrightnessUp": {
|
||||||
|
Key: "XF86MonBrightnessUp",
|
||||||
|
Action: `spawn dms ipc call brightness increment 5 ""`,
|
||||||
|
Description: "Brightness Up",
|
||||||
|
},
|
||||||
|
"XF86MonBrightnessDown": {
|
||||||
|
Key: "XF86MonBrightnessDown",
|
||||||
|
Action: `spawn dms ipc call brightness decrement 5 ""`,
|
||||||
|
Description: "Brightness Down",
|
||||||
|
},
|
||||||
|
"Super+Alt+Page_Up": {
|
||||||
|
Key: "Super+Alt+Page_Up",
|
||||||
|
Action: `spawn dms ipc call dash toggle ""`,
|
||||||
|
Description: "Dashboard Toggle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
content := provider.generateBindsContent(binds)
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create dms directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bindsFile := filepath.Join(dmsDir, "binds.kdl")
|
||||||
|
if err := os.WriteFile(bindsFile, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write binds file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testProvider := NewNiriProvider(tmpDir)
|
||||||
|
loadedBinds, err := testProvider.loadOverrideBinds()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load binds: %v\nContent was:\n%s", err, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, expected := range binds {
|
||||||
|
loaded, ok := loadedBinds[key]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Missing bind for key %s", key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if loaded.Action != expected.Action {
|
||||||
|
t.Errorf("Action mismatch for %s:\n got: %q\n want: %q", key, loaded.Action, expected.Action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNiriProviderWithRealWorldConfig(t *testing.T) {
|
func TestNiriProviderWithRealWorldConfig(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configFile := filepath.Join(tmpDir, "config.kdl")
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type Keybind struct {
|
|||||||
Source string `json:"source,omitempty"`
|
Source string `json:"source,omitempty"`
|
||||||
HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
|
HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
|
||||||
CooldownMs int `json:"cooldownMs,omitempty"`
|
CooldownMs int `json:"cooldownMs,omitempty"`
|
||||||
|
Flags string `json:"flags,omitempty"` // Hyprland bind flags: e=repeat, l=locked, r=release, o=long-press
|
||||||
Conflict *Keybind `json:"conflict,omitempty"`
|
Conflict *Keybind `json:"conflict,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package network
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -925,25 +926,24 @@ func (b *NetworkManagerBackend) ImportVPN(filePath string, name string) (*VPNImp
|
|||||||
func (b *NetworkManagerBackend) importVPNWithNmcli(filePath string, name string) (*VPNImportResult, error) {
|
func (b *NetworkManagerBackend) importVPNWithNmcli(filePath string, name string) (*VPNImportResult, error) {
|
||||||
vpnTypes := []string{"openvpn", "wireguard", "vpnc", "pptp", "l2tp", "openconnect", "strongswan"}
|
vpnTypes := []string{"openvpn", "wireguard", "vpnc", "pptp", "l2tp", "openconnect", "strongswan"}
|
||||||
|
|
||||||
var output []byte
|
var allErrors []error
|
||||||
var err error
|
var outputStr string
|
||||||
for _, vpnType := range vpnTypes {
|
for _, vpnType := range vpnTypes {
|
||||||
args := []string{"connection", "import", "type", vpnType, "file", filePath}
|
cmd := exec.Command("nmcli", "connection", "import", "type", vpnType, "file", filePath)
|
||||||
cmd := exec.Command("nmcli", args...)
|
output, err := cmd.CombinedOutput()
|
||||||
output, err = cmd.CombinedOutput()
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
outputStr = string(output)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
allErrors = append(allErrors, fmt.Errorf("%s: %s", vpnType, strings.TrimSpace(string(output))))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if len(allErrors) == len(vpnTypes) {
|
||||||
return &VPNImportResult{
|
return &VPNImportResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Error: fmt.Sprintf("import failed: %s", strings.TrimSpace(string(output))),
|
Error: errors.Join(allErrors...).Error(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
outputStr := string(output)
|
|
||||||
var connUUID, connName string
|
var connUUID, connName string
|
||||||
|
|
||||||
lines := strings.Split(outputStr, "\n")
|
lines := strings.Split(outputStr, "\n")
|
||||||
|
|||||||
@@ -357,31 +357,51 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
|||||||
|
|
||||||
savedSSIDs := make(map[string]bool)
|
savedSSIDs := make(map[string]bool)
|
||||||
autoconnectMap := make(map[string]bool)
|
autoconnectMap := make(map[string]bool)
|
||||||
|
hiddenSSIDs := make(map[string]bool)
|
||||||
for _, conn := range connections {
|
for _, conn := range connections {
|
||||||
connSettings, err := conn.GetSettings()
|
connSettings, err := conn.GetSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if connMeta, ok := connSettings["connection"]; ok {
|
connMeta, ok := connSettings["connection"]
|
||||||
if connType, ok := connMeta["type"].(string); ok && connType == "802-11-wireless" {
|
if !ok {
|
||||||
if wifiSettings, ok := connSettings["802-11-wireless"]; ok {
|
continue
|
||||||
if ssidBytes, ok := wifiSettings["ssid"].([]byte); ok {
|
}
|
||||||
ssid := string(ssidBytes)
|
|
||||||
savedSSIDs[ssid] = true
|
connType, ok := connMeta["type"].(string)
|
||||||
autoconnect := true
|
if !ok || connType != "802-11-wireless" {
|
||||||
if ac, ok := connMeta["autoconnect"].(bool); ok {
|
continue
|
||||||
autoconnect = ac
|
}
|
||||||
}
|
|
||||||
autoconnectMap[ssid] = autoconnect
|
wifiSettings, ok := connSettings["802-11-wireless"]
|
||||||
}
|
if !ok {
|
||||||
}
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ssidBytes, ok := wifiSettings["ssid"].([]byte)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ssid := string(ssidBytes)
|
||||||
|
savedSSIDs[ssid] = true
|
||||||
|
autoconnect := true
|
||||||
|
if ac, ok := connMeta["autoconnect"].(bool); ok {
|
||||||
|
autoconnect = ac
|
||||||
|
}
|
||||||
|
autoconnectMap[ssid] = autoconnect
|
||||||
|
|
||||||
|
if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden {
|
||||||
|
hiddenSSIDs[ssid] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b.stateMutex.RLock()
|
b.stateMutex.RLock()
|
||||||
currentSSID := b.state.WiFiSSID
|
currentSSID := b.state.WiFiSSID
|
||||||
|
wifiConnected := b.state.WiFiConnected
|
||||||
|
wifiSignal := b.state.WiFiSignal
|
||||||
|
wifiBSSID := b.state.WiFiBSSID
|
||||||
b.stateMutex.RUnlock()
|
b.stateMutex.RUnlock()
|
||||||
|
|
||||||
seenSSIDs := make(map[string]*WiFiNetwork)
|
seenSSIDs := make(map[string]*WiFiNetwork)
|
||||||
@@ -444,6 +464,7 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
|||||||
Connected: ssid == currentSSID,
|
Connected: ssid == currentSSID,
|
||||||
Saved: savedSSIDs[ssid],
|
Saved: savedSSIDs[ssid],
|
||||||
Autoconnect: autoconnectMap[ssid],
|
Autoconnect: autoconnectMap[ssid],
|
||||||
|
Hidden: hiddenSSIDs[ssid],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: maxBitrate / 1000,
|
||||||
@@ -454,6 +475,23 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
|||||||
networks = append(networks, network)
|
networks = append(networks, network)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if wifiConnected && currentSSID != "" {
|
||||||
|
if _, exists := seenSSIDs[currentSSID]; !exists {
|
||||||
|
hiddenNetwork := WiFiNetwork{
|
||||||
|
SSID: currentSSID,
|
||||||
|
BSSID: wifiBSSID,
|
||||||
|
Signal: wifiSignal,
|
||||||
|
Secured: true,
|
||||||
|
Connected: true,
|
||||||
|
Saved: savedSSIDs[currentSSID],
|
||||||
|
Autoconnect: autoconnectMap[currentSSID],
|
||||||
|
Hidden: true,
|
||||||
|
Mode: "infrastructure",
|
||||||
|
}
|
||||||
|
networks = append(networks, hiddenNetwork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sortWiFiNetworks(networks)
|
sortWiFiNetworks(networks)
|
||||||
|
|
||||||
b.stateMutex.Lock()
|
b.stateMutex.Lock()
|
||||||
@@ -515,40 +553,53 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
|
|||||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||||
dev := devInfo.device
|
dev := devInfo.device
|
||||||
w := devInfo.wireless
|
w := devInfo.wireless
|
||||||
apPaths, err := w.GetAccessPoints()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get access points: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetAP gonetworkmanager.AccessPoint
|
var targetAP gonetworkmanager.AccessPoint
|
||||||
for _, ap := range apPaths {
|
var flags, wpaFlags, rsnFlags uint32
|
||||||
ssid, err := ap.GetPropertySSID()
|
|
||||||
if err != nil || ssid != req.SSID {
|
if !req.Hidden {
|
||||||
continue
|
apPaths, err := w.GetAccessPoints()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get access points: %w", err)
|
||||||
}
|
}
|
||||||
targetAP = ap
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if targetAP == nil {
|
for _, ap := range apPaths {
|
||||||
return fmt.Errorf("access point not found: %s", req.SSID)
|
ssid, err := ap.GetPropertySSID()
|
||||||
}
|
if err != nil || ssid != req.SSID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
targetAP = ap
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
flags, _ := targetAP.GetPropertyFlags()
|
if targetAP == nil {
|
||||||
wpaFlags, _ := targetAP.GetPropertyWPAFlags()
|
return fmt.Errorf("access point not found: %s", req.SSID)
|
||||||
rsnFlags, _ := targetAP.GetPropertyRSNFlags()
|
}
|
||||||
|
|
||||||
|
flags, _ = targetAP.GetPropertyFlags()
|
||||||
|
wpaFlags, _ = targetAP.GetPropertyWPAFlags()
|
||||||
|
rsnFlags, _ = targetAP.GetPropertyRSNFlags()
|
||||||
|
}
|
||||||
|
|
||||||
const KeyMgmt8021x = uint32(512)
|
const KeyMgmt8021x = uint32(512)
|
||||||
const KeyMgmtPsk = uint32(256)
|
const KeyMgmtPsk = uint32(256)
|
||||||
const KeyMgmtSae = uint32(1024)
|
const KeyMgmtSae = uint32(1024)
|
||||||
|
|
||||||
isEnterprise := (wpaFlags&KeyMgmt8021x) != 0 || (rsnFlags&KeyMgmt8021x) != 0
|
var isEnterprise, isPsk, isSae, secured bool
|
||||||
isPsk := (wpaFlags&KeyMgmtPsk) != 0 || (rsnFlags&KeyMgmtPsk) != 0
|
|
||||||
isSae := (wpaFlags&KeyMgmtSae) != 0 || (rsnFlags&KeyMgmtSae) != 0
|
|
||||||
|
|
||||||
secured := flags != uint32(gonetworkmanager.Nm80211APFlagsNone) ||
|
switch {
|
||||||
wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) ||
|
case req.Hidden:
|
||||||
rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone)
|
secured = req.Password != "" || req.Username != ""
|
||||||
|
isEnterprise = req.Username != ""
|
||||||
|
isPsk = req.Password != "" && !isEnterprise
|
||||||
|
default:
|
||||||
|
isEnterprise = (wpaFlags&KeyMgmt8021x) != 0 || (rsnFlags&KeyMgmt8021x) != 0
|
||||||
|
isPsk = (wpaFlags&KeyMgmtPsk) != 0 || (rsnFlags&KeyMgmtPsk) != 0
|
||||||
|
isSae = (wpaFlags&KeyMgmtSae) != 0 || (rsnFlags&KeyMgmtSae) != 0
|
||||||
|
secured = flags != uint32(gonetworkmanager.Nm80211APFlagsNone) ||
|
||||||
|
wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) ||
|
||||||
|
rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone)
|
||||||
|
}
|
||||||
|
|
||||||
if isEnterprise {
|
if isEnterprise {
|
||||||
log.Infof("[createAndConnectWiFi] Enterprise network detected (802.1x) - SSID: %s, interactive: %v",
|
log.Infof("[createAndConnectWiFi] Enterprise network detected (802.1x) - SSID: %s, interactive: %v",
|
||||||
@@ -567,11 +618,15 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
|
|||||||
settings["ipv6"] = map[string]any{"method": "auto"}
|
settings["ipv6"] = map[string]any{"method": "auto"}
|
||||||
|
|
||||||
if secured {
|
if secured {
|
||||||
settings["802-11-wireless"] = map[string]any{
|
wifiSettings := map[string]any{
|
||||||
"ssid": []byte(req.SSID),
|
"ssid": []byte(req.SSID),
|
||||||
"mode": "infrastructure",
|
"mode": "infrastructure",
|
||||||
"security": "802-11-wireless-security",
|
"security": "802-11-wireless-security",
|
||||||
}
|
}
|
||||||
|
if req.Hidden {
|
||||||
|
wifiSettings["hidden"] = true
|
||||||
|
}
|
||||||
|
settings["802-11-wireless"] = wifiSettings
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case isEnterprise || req.Username != "":
|
case isEnterprise || req.Username != "":
|
||||||
@@ -658,10 +713,14 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
|
|||||||
return fmt.Errorf("secured network but not SAE/PSK/802.1X (rsn=0x%x wpa=0x%x)", rsnFlags, wpaFlags)
|
return fmt.Errorf("secured network but not SAE/PSK/802.1X (rsn=0x%x wpa=0x%x)", rsnFlags, wpaFlags)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
settings["802-11-wireless"] = map[string]any{
|
wifiSettings := map[string]any{
|
||||||
"ssid": []byte(req.SSID),
|
"ssid": []byte(req.SSID),
|
||||||
"mode": "infrastructure",
|
"mode": "infrastructure",
|
||||||
}
|
}
|
||||||
|
if req.Hidden {
|
||||||
|
wifiSettings["hidden"] = true
|
||||||
|
}
|
||||||
|
settings["802-11-wireless"] = wifiSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Interactive {
|
if req.Interactive {
|
||||||
@@ -685,14 +744,23 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
|
|||||||
log.Infof("[createAndConnectWiFi] Enterprise connection added, activating (secret agent will be called)")
|
log.Infof("[createAndConnectWiFi] Enterprise connection added, activating (secret agent will be called)")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = nm.ActivateWirelessConnection(conn, dev, targetAP)
|
if req.Hidden {
|
||||||
|
_, err = nm.ActivateConnection(conn, dev, nil)
|
||||||
|
} else {
|
||||||
|
_, err = nm.ActivateWirelessConnection(conn, dev, targetAP)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to activate connection: %w", err)
|
return fmt.Errorf("failed to activate connection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("[createAndConnectWiFi] Connection activation initiated, waiting for NetworkManager state changes...")
|
log.Infof("[createAndConnectWiFi] Connection activation initiated, waiting for NetworkManager state changes...")
|
||||||
} else {
|
} else {
|
||||||
_, err = nm.AddAndActivateWirelessConnection(settings, dev, targetAP)
|
var err error
|
||||||
|
if req.Hidden {
|
||||||
|
_, err = nm.AddAndActivateConnection(settings, dev)
|
||||||
|
} else {
|
||||||
|
_, err = nm.AddAndActivateWirelessConnection(settings, dev, targetAP)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to connect: %w", err)
|
return fmt.Errorf("failed to connect: %w", err)
|
||||||
}
|
}
|
||||||
@@ -813,6 +881,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
|
|
||||||
savedSSIDs := make(map[string]bool)
|
savedSSIDs := make(map[string]bool)
|
||||||
autoconnectMap := make(map[string]bool)
|
autoconnectMap := make(map[string]bool)
|
||||||
|
hiddenSSIDs := make(map[string]bool)
|
||||||
for _, conn := range connections {
|
for _, conn := range connections {
|
||||||
connSettings, err := conn.GetSettings()
|
connSettings, err := conn.GetSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -846,6 +915,10 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
autoconnect = ac
|
autoconnect = ac
|
||||||
}
|
}
|
||||||
autoconnectMap[ssid] = autoconnect
|
autoconnectMap[ssid] = autoconnect
|
||||||
|
|
||||||
|
if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden {
|
||||||
|
hiddenSSIDs[ssid] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var devices []WiFiDevice
|
var devices []WiFiDevice
|
||||||
@@ -939,6 +1012,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
Connected: connected && apSSID == ssid,
|
Connected: connected && apSSID == ssid,
|
||||||
Saved: savedSSIDs[apSSID],
|
Saved: savedSSIDs[apSSID],
|
||||||
Autoconnect: autoconnectMap[apSSID],
|
Autoconnect: autoconnectMap[apSSID],
|
||||||
|
Hidden: hiddenSSIDs[apSSID],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: maxBitrate / 1000,
|
||||||
@@ -949,6 +1023,25 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
seenSSIDs[apSSID] = &network
|
seenSSIDs[apSSID] = &network
|
||||||
networks = append(networks, network)
|
networks = append(networks, network)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if connected && ssid != "" {
|
||||||
|
if _, exists := seenSSIDs[ssid]; !exists {
|
||||||
|
hiddenNetwork := WiFiNetwork{
|
||||||
|
SSID: ssid,
|
||||||
|
BSSID: bssid,
|
||||||
|
Signal: signal,
|
||||||
|
Secured: true,
|
||||||
|
Connected: true,
|
||||||
|
Saved: savedSSIDs[ssid],
|
||||||
|
Autoconnect: autoconnectMap[ssid],
|
||||||
|
Hidden: true,
|
||||||
|
Mode: "infrastructure",
|
||||||
|
Device: name,
|
||||||
|
}
|
||||||
|
networks = append(networks, hiddenNetwork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sortWiFiNetworks(networks)
|
sortWiFiNetworks(networks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type WiFiNetwork struct {
|
|||||||
Connected bool `json:"connected"`
|
Connected bool `json:"connected"`
|
||||||
Saved bool `json:"saved"`
|
Saved bool `json:"saved"`
|
||||||
Autoconnect bool `json:"autoconnect"`
|
Autoconnect bool `json:"autoconnect"`
|
||||||
|
Hidden bool `json:"hidden"`
|
||||||
Frequency uint32 `json:"frequency"`
|
Frequency uint32 `json:"frequency"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
Rate uint32 `json:"rate"`
|
Rate uint32 `json:"rate"`
|
||||||
@@ -127,6 +128,7 @@ type ConnectionRequest struct {
|
|||||||
AnonymousIdentity string `json:"anonymousIdentity,omitempty"`
|
AnonymousIdentity string `json:"anonymousIdentity,omitempty"`
|
||||||
DomainSuffixMatch string `json:"domainSuffixMatch,omitempty"`
|
DomainSuffixMatch string `json:"domainSuffixMatch,omitempty"`
|
||||||
Interactive bool `json:"interactive,omitempty"`
|
Interactive bool `json:"interactive,omitempty"`
|
||||||
|
Hidden bool `json:"hidden,omitempty"`
|
||||||
Device string `json:"device,omitempty"`
|
Device string `json:"device,omitempty"`
|
||||||
EAPMethod string `json:"eapMethod,omitempty"`
|
EAPMethod string `json:"eapMethod,omitempty"`
|
||||||
Phase2Auth string `json:"phase2Auth,omitempty"`
|
Phase2Auth string `json:"phase2Auth,omitempty"`
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ const KEY_MAP = {
|
|||||||
16777349: "XF86AudioMedia",
|
16777349: "XF86AudioMedia",
|
||||||
16777350: "XF86AudioRecord",
|
16777350: "XF86AudioRecord",
|
||||||
16842798: "XF86MonBrightnessUp",
|
16842798: "XF86MonBrightnessUp",
|
||||||
|
16777394: "XF86MonBrightnessUp",
|
||||||
16842797: "XF86MonBrightnessDown",
|
16842797: "XF86MonBrightnessDown",
|
||||||
|
16777395: "XF86MonBrightnessDown",
|
||||||
16842800: "XF86KbdBrightnessUp",
|
16842800: "XF86KbdBrightnessUp",
|
||||||
16842799: "XF86KbdBrightnessDown",
|
16842799: "XF86KbdBrightnessDown",
|
||||||
16842796: "XF86PowerOff",
|
16842796: "XF86PowerOff",
|
||||||
|
|||||||
@@ -1587,6 +1587,9 @@ Singleton {
|
|||||||
updateCompositorCursor();
|
updateCompositorCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This solution for xwayland cursor themes is from the xwls discussion:
|
||||||
|
// https://github.com/Supreeeme/xwayland-satellite/issues/104
|
||||||
|
// no idea if this matters on other compositors but we also set XCURSOR stuff in the launcher
|
||||||
function updateCompositorCursor() {
|
function updateCompositorCursor() {
|
||||||
updateXResources();
|
updateXResources();
|
||||||
if (typeof CompositorService === "undefined")
|
if (typeof CompositorService === "undefined")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals
|
import qs.Modals
|
||||||
|
import qs.Modals.Changelog
|
||||||
import qs.Modals.Clipboard
|
import qs.Modals.Clipboard
|
||||||
import qs.Modals.Greeter
|
import qs.Modals.Greeter
|
||||||
import qs.Modals.Settings
|
import qs.Modals.Settings
|
||||||
@@ -836,9 +837,29 @@ Item {
|
|||||||
function onGreeterRequested() {
|
function onGreeterRequested() {
|
||||||
if (greeterLoader.active && greeterLoader.item) {
|
if (greeterLoader.active && greeterLoader.item) {
|
||||||
greeterLoader.item.show();
|
greeterLoader.item.show();
|
||||||
} else {
|
return;
|
||||||
greeterLoader.active = true;
|
|
||||||
}
|
}
|
||||||
|
greeterLoader.active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: changelogLoader
|
||||||
|
active: false
|
||||||
|
sourceComponent: ChangelogModal {
|
||||||
|
onChangelogDismissed: changelogLoader.active = false
|
||||||
|
Component.onCompleted: show()
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: ChangelogService
|
||||||
|
function onChangelogRequested() {
|
||||||
|
if (changelogLoader.active && changelogLoader.item) {
|
||||||
|
changelogLoader.item.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
changelogLoader.active = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
246
quickshell/Modals/Changelog/ChangelogContent.qml
Normal file
246
quickshell/Modals/Changelog/ChangelogContent.qml
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property real logoSize: Math.round(Theme.iconSize * 2.8)
|
||||||
|
readonly property real badgeHeight: Math.round(Theme.fontSizeSmall * 1.7)
|
||||||
|
|
||||||
|
topPadding: Theme.spacingL
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Image {
|
||||||
|
width: root.logoSize
|
||||||
|
height: width * (569.94629 / 506.50931)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
|
asynchronous: true
|
||||||
|
source: "file://" + Theme.shellDir + "/assets/danklogonormal.svg"
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: true
|
||||||
|
layer.mipmap: true
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
saturation: 0
|
||||||
|
colorization: 1
|
||||||
|
colorizationColor: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "DMS " + ChangelogService.currentVersion
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge + 2
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: codenameText.implicitWidth + Theme.spacingM * 2
|
||||||
|
height: root.badgeHeight
|
||||||
|
radius: root.badgeHeight / 2
|
||||||
|
color: Theme.primaryContainer
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: codenameText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Spicy Miso"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Desktop widgets, theme registry, native clipboard & more"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Theme.outlineMedium
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "What's New"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
Grid {
|
||||||
|
width: parent.width
|
||||||
|
columns: 2
|
||||||
|
rowSpacing: Theme.spacingS
|
||||||
|
columnSpacing: Theme.spacingS
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "widgets"
|
||||||
|
title: "Desktop Widgets"
|
||||||
|
description: "Widgets on your desktop"
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("desktop_widgets")
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "palette"
|
||||||
|
title: "Theme Registry"
|
||||||
|
description: "Community themes"
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("theme")
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "content_paste"
|
||||||
|
title: "Native Clipboard"
|
||||||
|
description: "Zero-dependency history"
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("clipboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "display_settings"
|
||||||
|
title: "Monitor Config"
|
||||||
|
description: "Full display setup"
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("display_config")
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "notifications_active"
|
||||||
|
title: "Notifications"
|
||||||
|
description: "History & gestures"
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("notifications")
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "healing"
|
||||||
|
title: "DMS Doctor"
|
||||||
|
description: "Diagnose issues"
|
||||||
|
onClicked: FirstLaunchService.showDoctor()
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "keyboard"
|
||||||
|
title: "Keybinds Editor"
|
||||||
|
description: "niri, Hyprland, & MangoWC"
|
||||||
|
visible: KeybindsService.available
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("keybinds")
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "search"
|
||||||
|
title: "Settings Search"
|
||||||
|
description: "Find settings fast"
|
||||||
|
onClicked: PopoutService.openSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Theme.outlineMedium
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "warning"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.warning
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Upgrade Notes"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: upgradeNotesColumn.height + Theme.spacingM * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.warning, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.withAlpha(Theme.warning, 0.2)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: upgradeNotesColumn
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
ChangelogUpgradeNote {
|
||||||
|
width: parent.width
|
||||||
|
text: "Ghostty theme path changed to ~/.config/ghostty/themes/danktheme"
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogUpgradeNote {
|
||||||
|
width: parent.width
|
||||||
|
text: "VS Code theme reinstall required"
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogUpgradeNote {
|
||||||
|
width: parent.width
|
||||||
|
text: "Clipboard history migration available from cliphist"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "See full release notes for migration steps"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
quickshell/Modals/Changelog/ChangelogFeatureCard.qml
Normal file
78
quickshell/Modals/Changelog/ChangelogFeatureCard.qml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string iconName: ""
|
||||||
|
property string title: ""
|
||||||
|
property string description: ""
|
||||||
|
|
||||||
|
signal clicked
|
||||||
|
|
||||||
|
readonly property real iconContainerSize: Math.round(Theme.iconSize * 1.3)
|
||||||
|
|
||||||
|
height: Math.round(Theme.fontSizeMedium * 4.2)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: parent.radius
|
||||||
|
color: Theme.primary
|
||||||
|
opacity: mouseArea.containsMouse ? 0.12 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: root.iconContainerSize
|
||||||
|
height: root.iconContainerSize
|
||||||
|
radius: Math.round(root.iconContainerSize * 0.28)
|
||||||
|
color: Theme.primaryContainer
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: root.iconName
|
||||||
|
size: Theme.iconSize - 6
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
width: parent.width - root.iconContainerSize - Theme.spacingS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.title
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.description
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.clicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
155
quickshell/Modals/Changelog/ChangelogModal.qml
Normal file
155
quickshell/Modals/Changelog/ChangelogModal.qml
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
FloatingWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property int modalWidth: 680
|
||||||
|
readonly property int modalHeight: screen ? Math.min(720, screen.height - 80) : 720
|
||||||
|
|
||||||
|
signal changelogDismissed
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
objectName: "changelogModal"
|
||||||
|
title: "What's New"
|
||||||
|
minimumSize: Qt.size(modalWidth, modalHeight)
|
||||||
|
maximumSize: Qt.size(modalWidth, modalHeight)
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
id: contentFocusScope
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
Keys.onEscapePressed: event => {
|
||||||
|
root.dismiss();
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
root.dismiss();
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
height: headerRow.height + Theme.spacingM
|
||||||
|
onPressed: windowControls.tryStartMove()
|
||||||
|
onDoubleClicked: windowControls.tryToggleMaximize()
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
height: Math.round(Theme.fontSizeMedium * 2.85)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
visible: windowControls.supported && windowControls.canMaximize
|
||||||
|
iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: windowControls.tryToggleMaximize()
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: root.dismiss()
|
||||||
|
|
||||||
|
DankTooltip {
|
||||||
|
text: "Close"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.bottom: footerRow.top
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
clip: true
|
||||||
|
contentHeight: mainColumn.height + Theme.spacingL * 2
|
||||||
|
contentWidth: width
|
||||||
|
|
||||||
|
ChangelogContent {
|
||||||
|
id: mainColumn
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: Math.min(600, parent.width - Theme.spacingXL * 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: footerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: Math.round(Theme.fontSizeMedium * 4.5)
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.top: parent.top
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Theme.outlineMedium
|
||||||
|
opacity: 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: "Read Full Release Notes"
|
||||||
|
iconName: "open_in_new"
|
||||||
|
backgroundColor: Theme.surfaceContainerHighest
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/dms-1-2-spicy-miso")
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: "Got It"
|
||||||
|
iconName: "check"
|
||||||
|
backgroundColor: Theme.primary
|
||||||
|
textColor: Theme.primaryText
|
||||||
|
onClicked: root.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingWindowControls {
|
||||||
|
id: windowControls
|
||||||
|
targetWindow: root
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
ChangelogService.dismissChangelog();
|
||||||
|
changelogDismissed();
|
||||||
|
visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
quickshell/Modals/Changelog/ChangelogUpgradeNote.qml
Normal file
27
quickshell/Modals/Changelog/ChangelogUpgradeNote.qml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias text: noteText.text
|
||||||
|
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "arrow_right"
|
||||||
|
size: Theme.iconSizeSmall - 2
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: noteText
|
||||||
|
width: root.width - Theme.iconSizeSmall - Theme.spacingS
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,12 +9,21 @@ Rectangle {
|
|||||||
property string title: ""
|
property string title: ""
|
||||||
property string description: ""
|
property string description: ""
|
||||||
|
|
||||||
|
signal clicked
|
||||||
|
|
||||||
readonly property real iconContainerSize: Math.round(Theme.iconSize * 1.5)
|
readonly property real iconContainerSize: Math.round(Theme.iconSize * 1.5)
|
||||||
|
|
||||||
height: Math.round(Theme.fontSizeMedium * 6.4)
|
height: Math.round(Theme.fontSizeMedium * 6.4)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceContainerHigh
|
color: Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: parent.radius
|
||||||
|
color: Theme.primary
|
||||||
|
opacity: mouseArea.containsMouse ? 0.12 : 0
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
@@ -54,4 +63,12 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.clicked()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -87,6 +89,7 @@ Item {
|
|||||||
iconName: "auto_awesome"
|
iconName: "auto_awesome"
|
||||||
title: I18n.tr("Dynamic Theming", "greeter feature card title")
|
title: I18n.tr("Dynamic Theming", "greeter feature card title")
|
||||||
description: I18n.tr("Colors from wallpaper", "greeter feature card description")
|
description: I18n.tr("Colors from wallpaper", "greeter feature card description")
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("theme")
|
||||||
}
|
}
|
||||||
|
|
||||||
GreeterFeatureCard {
|
GreeterFeatureCard {
|
||||||
@@ -94,6 +97,7 @@ Item {
|
|||||||
iconName: "format_paint"
|
iconName: "format_paint"
|
||||||
title: I18n.tr("App Theming", "greeter feature card title")
|
title: I18n.tr("App Theming", "greeter feature card title")
|
||||||
description: I18n.tr("GTK, Qt, IDEs, more", "greeter feature card description")
|
description: I18n.tr("GTK, Qt, IDEs, more", "greeter feature card description")
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("theme")
|
||||||
}
|
}
|
||||||
|
|
||||||
GreeterFeatureCard {
|
GreeterFeatureCard {
|
||||||
@@ -101,6 +105,7 @@ Item {
|
|||||||
iconName: "download"
|
iconName: "download"
|
||||||
title: I18n.tr("Theme Registry", "greeter feature card title")
|
title: I18n.tr("Theme Registry", "greeter feature card title")
|
||||||
description: I18n.tr("Community themes", "greeter feature card description")
|
description: I18n.tr("Community themes", "greeter feature card description")
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("theme")
|
||||||
}
|
}
|
||||||
|
|
||||||
GreeterFeatureCard {
|
GreeterFeatureCard {
|
||||||
@@ -108,6 +113,7 @@ Item {
|
|||||||
iconName: "view_carousel"
|
iconName: "view_carousel"
|
||||||
title: I18n.tr("DankBar", "greeter feature card title")
|
title: I18n.tr("DankBar", "greeter feature card title")
|
||||||
description: I18n.tr("Modular widget bar", "greeter feature card description")
|
description: I18n.tr("Modular widget bar", "greeter feature card description")
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("dankbar_settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
GreeterFeatureCard {
|
GreeterFeatureCard {
|
||||||
@@ -115,6 +121,7 @@ Item {
|
|||||||
iconName: "extension"
|
iconName: "extension"
|
||||||
title: I18n.tr("Plugins", "greeter feature card title")
|
title: I18n.tr("Plugins", "greeter feature card title")
|
||||||
description: I18n.tr("Extensible architecture", "greeter feature card description")
|
description: I18n.tr("Extensible architecture", "greeter feature card description")
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("plugins")
|
||||||
}
|
}
|
||||||
|
|
||||||
GreeterFeatureCard {
|
GreeterFeatureCard {
|
||||||
@@ -122,6 +129,10 @@ Item {
|
|||||||
iconName: "layers"
|
iconName: "layers"
|
||||||
title: I18n.tr("Multi-Monitor", "greeter feature card title")
|
title: I18n.tr("Multi-Monitor", "greeter feature card title")
|
||||||
description: I18n.tr("Per-screen config", "greeter feature card description")
|
description: I18n.tr("Per-screen config", "greeter feature card description")
|
||||||
|
onClicked: {
|
||||||
|
const hasDisplayConfig = CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl;
|
||||||
|
PopoutService.openSettingsWithTab(hasDisplayConfig ? "display_config" : "display_widgets");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GreeterFeatureCard {
|
GreeterFeatureCard {
|
||||||
@@ -129,6 +140,7 @@ Item {
|
|||||||
iconName: "nightlight"
|
iconName: "nightlight"
|
||||||
title: I18n.tr("Display Control", "greeter feature card title")
|
title: I18n.tr("Display Control", "greeter feature card title")
|
||||||
description: I18n.tr("Night mode & gamma", "greeter feature card description")
|
description: I18n.tr("Night mode & gamma", "greeter feature card description")
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("display_gamma")
|
||||||
}
|
}
|
||||||
|
|
||||||
GreeterFeatureCard {
|
GreeterFeatureCard {
|
||||||
@@ -136,13 +148,16 @@ Item {
|
|||||||
iconName: "tune"
|
iconName: "tune"
|
||||||
title: I18n.tr("Control Center", "greeter feature card title")
|
title: I18n.tr("Control Center", "greeter feature card title")
|
||||||
description: I18n.tr("Quick system toggles", "greeter feature card description")
|
description: I18n.tr("Quick system toggles", "greeter feature card description")
|
||||||
|
// This is doing an IPC since its just easier and lazier to access the bar ref
|
||||||
|
onClicked: Quickshell.execDetached(["dms", "ipc", "call", "control-center", "open"])
|
||||||
}
|
}
|
||||||
|
|
||||||
GreeterFeatureCard {
|
GreeterFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS * 2) / 3
|
width: (parent.width - Theme.spacingS * 2) / 3
|
||||||
iconName: "density_small"
|
iconName: "lock"
|
||||||
title: I18n.tr("System Tray", "greeter feature card title")
|
title: I18n.tr("Lock Screen", "greeter feature card title")
|
||||||
description: I18n.tr("Background app icons", "greeter feature card description")
|
description: I18n.tr("Security & privacy", "greeter feature card description")
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("lock_screen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,15 +34,19 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showWithTab(tabIndex: int) {
|
function showWithTab(tabIndex: int) {
|
||||||
if (tabIndex >= 0)
|
if (tabIndex >= 0) {
|
||||||
currentTabIndex = tabIndex;
|
currentTabIndex = tabIndex;
|
||||||
|
sidebar.autoExpandForTab(tabIndex);
|
||||||
|
}
|
||||||
visible = true;
|
visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showWithTabName(tabName: string) {
|
function showWithTabName(tabName: string) {
|
||||||
var idx = sidebar.resolveTabIndex(tabName);
|
var idx = sidebar.resolveTabIndex(tabName);
|
||||||
if (idx >= 0)
|
if (idx >= 0) {
|
||||||
currentTabIndex = idx;
|
currentTabIndex = idx;
|
||||||
|
sidebar.autoExpandForTab(idx);
|
||||||
|
}
|
||||||
visible = true;
|
visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ FloatingWindow {
|
|||||||
property string wifiPasswordInput: ""
|
property string wifiPasswordInput: ""
|
||||||
property string wifiUsernameInput: ""
|
property string wifiUsernameInput: ""
|
||||||
property bool requiresEnterprise: false
|
property bool requiresEnterprise: false
|
||||||
|
property bool isHiddenNetwork: false
|
||||||
|
|
||||||
property string wifiAnonymousIdentityInput: ""
|
property string wifiAnonymousIdentityInput: ""
|
||||||
property string wifiDomainInput: ""
|
property string wifiDomainInput: ""
|
||||||
@@ -44,6 +45,8 @@ FloatingWindow {
|
|||||||
property int calculatedHeight: {
|
property int calculatedHeight: {
|
||||||
let h = headerHeight + buttonRowHeight + Theme.spacingL * 2;
|
let h = headerHeight + buttonRowHeight + Theme.spacingL * 2;
|
||||||
h += fieldsInfo.length * inputFieldWithSpacing;
|
h += fieldsInfo.length * inputFieldWithSpacing;
|
||||||
|
if (isHiddenNetwork)
|
||||||
|
h += inputFieldWithSpacing;
|
||||||
if (showUsernameField)
|
if (showUsernameField)
|
||||||
h += inputFieldWithSpacing;
|
h += inputFieldWithSpacing;
|
||||||
if (showPasswordField)
|
if (showPasswordField)
|
||||||
@@ -68,6 +71,10 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isHiddenNetwork) {
|
||||||
|
ssidInput.forceActiveFocus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (requiresEnterprise && !isVpnPrompt) {
|
if (requiresEnterprise && !isVpnPrompt) {
|
||||||
usernameInput.forceActiveFocus();
|
usernameInput.forceActiveFocus();
|
||||||
return;
|
return;
|
||||||
@@ -82,6 +89,7 @@ FloatingWindow {
|
|||||||
wifiAnonymousIdentityInput = "";
|
wifiAnonymousIdentityInput = "";
|
||||||
wifiDomainInput = "";
|
wifiDomainInput = "";
|
||||||
isPromptMode = false;
|
isPromptMode = false;
|
||||||
|
isHiddenNetwork = false;
|
||||||
promptToken = "";
|
promptToken = "";
|
||||||
promptReason = "";
|
promptReason = "";
|
||||||
promptFields = [];
|
promptFields = [];
|
||||||
@@ -100,6 +108,30 @@ FloatingWindow {
|
|||||||
Qt.callLater(focusFirstField);
|
Qt.callLater(focusFirstField);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showHidden() {
|
||||||
|
wifiPasswordSSID = "";
|
||||||
|
wifiPasswordInput = "";
|
||||||
|
wifiUsernameInput = "";
|
||||||
|
wifiAnonymousIdentityInput = "";
|
||||||
|
wifiDomainInput = "";
|
||||||
|
isPromptMode = false;
|
||||||
|
isHiddenNetwork = true;
|
||||||
|
promptToken = "";
|
||||||
|
promptReason = "";
|
||||||
|
promptFields = [];
|
||||||
|
promptSetting = "";
|
||||||
|
isVpnPrompt = false;
|
||||||
|
connectionName = "";
|
||||||
|
vpnServiceType = "";
|
||||||
|
connectionType = "";
|
||||||
|
fieldsInfo = [];
|
||||||
|
secretValues = {};
|
||||||
|
requiresEnterprise = false;
|
||||||
|
|
||||||
|
visible = true;
|
||||||
|
Qt.callLater(focusFirstField);
|
||||||
|
}
|
||||||
|
|
||||||
function showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fInfo) {
|
function showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fInfo) {
|
||||||
isPromptMode = true;
|
isPromptMode = true;
|
||||||
promptToken = token;
|
promptToken = token;
|
||||||
@@ -184,8 +216,9 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
NetworkService.submitCredentials(promptToken, secrets, savePasswordCheckbox.checked);
|
NetworkService.submitCredentials(promptToken, secrets, savePasswordCheckbox.checked);
|
||||||
} else {
|
} else {
|
||||||
|
const ssid = isHiddenNetwork ? ssidInput.text : wifiPasswordSSID;
|
||||||
const username = requiresEnterprise ? usernameInput.text : "";
|
const username = requiresEnterprise ? usernameInput.text : "";
|
||||||
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username, wifiAnonymousIdentityInput, wifiDomainInput);
|
NetworkService.connectToWifi(ssid, passwordInput.text, username, wifiAnonymousIdentityInput, wifiDomainInput, isHiddenNetwork);
|
||||||
}
|
}
|
||||||
|
|
||||||
hide();
|
hide();
|
||||||
@@ -196,6 +229,8 @@ FloatingWindow {
|
|||||||
passwordInput.text = "";
|
passwordInput.text = "";
|
||||||
if (requiresEnterprise)
|
if (requiresEnterprise)
|
||||||
usernameInput.text = "";
|
usernameInput.text = "";
|
||||||
|
if (isHiddenNetwork)
|
||||||
|
ssidInput.text = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAndClose() {
|
function clearAndClose() {
|
||||||
@@ -215,6 +250,8 @@ FloatingWindow {
|
|||||||
return I18n.tr("Smartcard PIN");
|
return I18n.tr("Smartcard PIN");
|
||||||
if (isVpnPrompt)
|
if (isVpnPrompt)
|
||||||
return I18n.tr("VPN Password");
|
return I18n.tr("VPN Password");
|
||||||
|
if (isHiddenNetwork)
|
||||||
|
return I18n.tr("Hidden Network");
|
||||||
return I18n.tr("Wi-Fi Password");
|
return I18n.tr("Wi-Fi Password");
|
||||||
}
|
}
|
||||||
minimumSize: Qt.size(420, calculatedHeight)
|
minimumSize: Qt.size(420, calculatedHeight)
|
||||||
@@ -236,6 +273,7 @@ FloatingWindow {
|
|||||||
usernameInput.text = "";
|
usernameInput.text = "";
|
||||||
anonInput.text = "";
|
anonInput.text = "";
|
||||||
domainMatchInput.text = "";
|
domainMatchInput.text = "";
|
||||||
|
ssidInput.text = "";
|
||||||
for (var i = 0; i < dynamicFieldsRepeater.count; i++) {
|
for (var i = 0; i < dynamicFieldsRepeater.count; i++) {
|
||||||
const item = dynamicFieldsRepeater.itemAt(i);
|
const item = dynamicFieldsRepeater.itemAt(i);
|
||||||
if (item?.children[0])
|
if (item?.children[0])
|
||||||
@@ -296,6 +334,8 @@ FloatingWindow {
|
|||||||
return I18n.tr("Smartcard Authentication");
|
return I18n.tr("Smartcard Authentication");
|
||||||
if (isVpnPrompt)
|
if (isVpnPrompt)
|
||||||
return I18n.tr("Connect to VPN");
|
return I18n.tr("Connect to VPN");
|
||||||
|
if (isHiddenNetwork)
|
||||||
|
return I18n.tr("Connect to Hidden Network");
|
||||||
return I18n.tr("Connect to Wi-Fi");
|
return I18n.tr("Connect to Wi-Fi");
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
@@ -315,6 +355,8 @@ FloatingWindow {
|
|||||||
return I18n.tr("Enter credentials for ") + wifiPasswordSSID;
|
return I18n.tr("Enter credentials for ") + wifiPasswordSSID;
|
||||||
if (isVpnPrompt)
|
if (isVpnPrompt)
|
||||||
return I18n.tr("Enter password for ") + wifiPasswordSSID;
|
return I18n.tr("Enter password for ") + wifiPasswordSSID;
|
||||||
|
if (isHiddenNetwork)
|
||||||
|
return I18n.tr("Enter network name and password");
|
||||||
const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ");
|
const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ");
|
||||||
return prefix + wifiPasswordSSID;
|
return prefix + wifiPasswordSSID;
|
||||||
}
|
}
|
||||||
@@ -357,6 +399,34 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: inputFieldHeight
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceHover
|
||||||
|
border.color: ssidInput.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||||
|
border.width: ssidInput.activeFocus ? 2 : 1
|
||||||
|
visible: isHiddenNetwork
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: ssidInput.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: ssidInput
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
placeholderText: I18n.tr("Network Name (SSID)")
|
||||||
|
backgroundColor: "transparent"
|
||||||
|
enabled: root.visible
|
||||||
|
keyNavigationTab: passwordInput
|
||||||
|
onAccepted: passwordInput.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: dynamicFieldsRepeater
|
id: dynamicFieldsRepeater
|
||||||
model: fieldsInfo
|
model: fieldsInfo
|
||||||
@@ -696,6 +766,8 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
if (isVpnPrompt)
|
if (isVpnPrompt)
|
||||||
return passwordInput.text.length > 0;
|
return passwordInput.text.length > 0;
|
||||||
|
if (isHiddenNetwork)
|
||||||
|
return ssidInput.text.length > 0;
|
||||||
return requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0;
|
return requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0;
|
||||||
}
|
}
|
||||||
opacity: enabled ? 1 : 0.5
|
opacity: enabled ? 1 : 0.5
|
||||||
|
|||||||
@@ -401,8 +401,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkIncludeStatus() {
|
function checkIncludeStatus() {
|
||||||
const paths = getConfigPaths();
|
const compositor = CompositorService.compositor;
|
||||||
if (!paths) {
|
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") {
|
||||||
includeStatus = {
|
includeStatus = {
|
||||||
"exists": false,
|
"exists": false,
|
||||||
"included": false
|
"included": false
|
||||||
@@ -410,14 +410,27 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filename = (compositor === "niri") ? "outputs.kdl" : "outputs.conf";
|
||||||
|
const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
|
||||||
|
|
||||||
checkingInclude = true;
|
checkingInclude = true;
|
||||||
Proc.runCommand("check-outputs-include", ["sh", "-c", `exists=false; included=false; ` + `[ -f "${paths.outputsFile}" ] && exists=true; ` + `[ -f "${paths.configFile}" ] && grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" | grep -q '${paths.grepPattern}' && included=true; ` + `echo "$exists $included"`], (output, exitCode) => {
|
Proc.runCommand("check-outputs-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => {
|
||||||
checkingInclude = false;
|
checkingInclude = false;
|
||||||
const parts = output.trim().split(" ");
|
if (exitCode !== 0) {
|
||||||
includeStatus = {
|
includeStatus = {
|
||||||
"exists": parts[0] === "true",
|
"exists": false,
|
||||||
"included": parts[1] === "true"
|
"included": false
|
||||||
};
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
includeStatus = JSON.parse(output.trim());
|
||||||
|
} catch (e) {
|
||||||
|
includeStatus = {
|
||||||
|
"exists": false,
|
||||||
|
"included": false
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ Rectangle {
|
|||||||
font.pixelSize: Math.max(10, Math.min(14, root.width * 0.12))
|
font.pixelSize: Math.max(10, Math.min(14, root.width * 0.12))
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: root.isConnected ? Theme.surfaceText : Theme.surfaceVariantText
|
color: root.isConnected ? Theme.surfaceText : Theme.surfaceVariantText
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
elide: Text.ElideMiddle
|
elide: Text.ElideMiddle
|
||||||
width: Math.min(implicitWidth, root.width - 8)
|
width: Math.min(implicitWidth, root.width - 8)
|
||||||
|
|||||||
@@ -768,6 +768,13 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "wifi_find"
|
||||||
|
buttonSize: 32
|
||||||
|
visible: NetworkService.backend === "networkmanager" && NetworkService.wifiEnabled && !NetworkService.wifiToggling
|
||||||
|
onClicked: PopoutService.showHiddenNetworkModal()
|
||||||
|
}
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
iconName: "refresh"
|
iconName: "refresh"
|
||||||
buttonSize: 32
|
buttonSize: 32
|
||||||
@@ -1102,6 +1109,14 @@ Item {
|
|||||||
visible: isPinned
|
visible: isPinned
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "visibility_off"
|
||||||
|
size: 14
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: modelData.hidden || false
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -1127,6 +1142,20 @@ Item {
|
|||||||
visible: modelData.saved
|
visible: modelData.saved
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "•"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: modelData.hidden || false
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Hidden")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: modelData.hidden || false
|
||||||
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: "•"
|
text: "•"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
|||||||
@@ -55,22 +55,36 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkCursorIncludeStatus() {
|
function checkCursorIncludeStatus() {
|
||||||
const paths = getCursorConfigPaths();
|
const compositor = CompositorService.compositor;
|
||||||
if (!paths) {
|
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") {
|
||||||
cursorIncludeStatus = {
|
cursorIncludeStatus = {
|
||||||
"exists": false,
|
"exists": false,
|
||||||
"included": false
|
"included": false
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filename = (compositor === "niri") ? "cursor.kdl" : "cursor.conf";
|
||||||
|
const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
|
||||||
|
|
||||||
checkingCursorInclude = true;
|
checkingCursorInclude = true;
|
||||||
Proc.runCommand("check-cursor-include", ["sh", "-c", `exists=false; included=false; ` + `[ -f "${paths.cursorFile}" ] && exists=true; ` + `[ -f "${paths.configFile}" ] && grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" | grep -q '${paths.grepPattern}' && included=true; ` + `echo "$exists $included"`], (output, exitCode) => {
|
Proc.runCommand("check-cursor-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => {
|
||||||
checkingCursorInclude = false;
|
checkingCursorInclude = false;
|
||||||
const parts = output.trim().split(" ");
|
if (exitCode !== 0) {
|
||||||
cursorIncludeStatus = {
|
cursorIncludeStatus = {
|
||||||
"exists": parts[0] === "true",
|
"exists": false,
|
||||||
"included": parts[1] === "true"
|
"included": false
|
||||||
};
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
cursorIncludeStatus = JSON.parse(output.trim());
|
||||||
|
} catch (e) {
|
||||||
|
cursorIncludeStatus = {
|
||||||
|
"exists": false,
|
||||||
|
"included": false
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
90
quickshell/Services/ChangelogService.qml
Normal file
90
quickshell/Services/ChangelogService.qml
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtCore
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property string currentVersion: "1.2"
|
||||||
|
readonly property bool changelogEnabled: false
|
||||||
|
|
||||||
|
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/DankMaterialShell"
|
||||||
|
readonly property string changelogMarkerPath: configDir + "/.changelog-" + currentVersion
|
||||||
|
|
||||||
|
property bool checkComplete: false
|
||||||
|
property bool changelogDismissed: false
|
||||||
|
|
||||||
|
readonly property bool shouldShowChangelog: {
|
||||||
|
if (!checkComplete)
|
||||||
|
return false;
|
||||||
|
if (!changelogEnabled)
|
||||||
|
return false;
|
||||||
|
if (changelogDismissed)
|
||||||
|
return false;
|
||||||
|
if (typeof FirstLaunchService !== "undefined" && FirstLaunchService.isFirstLaunch)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
signal changelogRequested
|
||||||
|
signal changelogCompleted
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (!changelogEnabled)
|
||||||
|
return;
|
||||||
|
changelogCheckProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showChangelog() {
|
||||||
|
changelogRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissChangelog() {
|
||||||
|
changelogDismissed = true;
|
||||||
|
touchMarkerProcess.running = true;
|
||||||
|
changelogCompleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: changelogCheckProcess
|
||||||
|
|
||||||
|
command: ["sh", "-c", "[ -f '" + changelogMarkerPath + "' ] && echo 'seen' || echo 'show'"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
onRead: data => {
|
||||||
|
const result = data.trim();
|
||||||
|
root.checkComplete = true;
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case "seen":
|
||||||
|
root.changelogDismissed = true;
|
||||||
|
break;
|
||||||
|
case "show":
|
||||||
|
if (typeof FirstLaunchService === "undefined" || !FirstLaunchService.isFirstLaunch) {
|
||||||
|
root.changelogRequested();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: touchMarkerProcess
|
||||||
|
|
||||||
|
command: ["sh", "-c", "mkdir -p '" + configDir + "' && touch '" + changelogMarkerPath + "'"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.warn("ChangelogService: Failed to create changelog marker");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -413,7 +413,7 @@ Singleton {
|
|||||||
scanWifi();
|
scanWifi();
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectToWifi(ssid, password = "", username = "", anonymousIdentity = "", domainSuffixMatch = "") {
|
function connectToWifi(ssid, password = "", username = "", anonymousIdentity = "", domainSuffixMatch = "", hidden = false) {
|
||||||
if (!networkAvailable || isConnecting)
|
if (!networkAvailable || isConnecting)
|
||||||
return;
|
return;
|
||||||
pendingConnectionSSID = ssid;
|
pendingConnectionSSID = ssid;
|
||||||
@@ -427,6 +427,8 @@ Singleton {
|
|||||||
};
|
};
|
||||||
if (effectiveWifiDevice)
|
if (effectiveWifiDevice)
|
||||||
params.device = effectiveWifiDevice;
|
params.device = effectiveWifiDevice;
|
||||||
|
if (hidden)
|
||||||
|
params.hidden = true;
|
||||||
|
|
||||||
if (DMSService.apiVersion >= 7) {
|
if (DMSService.apiVersion >= 7) {
|
||||||
if (password || username) {
|
if (password || username) {
|
||||||
@@ -611,8 +613,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectToWifiAndSetPreference(ssid, password, username = "", anonymousIdentity = "", domainSuffixMatch = "") {
|
function connectToWifiAndSetPreference(ssid, password, username = "", anonymousIdentity = "", domainSuffixMatch = "", hidden = false) {
|
||||||
connectToWifi(ssid, password, username, anonymousIdentity, domainSuffixMatch);
|
connectToWifi(ssid, password, username, anonymousIdentity, domainSuffixMatch, hidden);
|
||||||
setNetworkPreference("wifi");
|
setNetworkPreference("wifi");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ Singleton {
|
|||||||
property var _flatCache: []
|
property var _flatCache: []
|
||||||
property var displayList: []
|
property var displayList: []
|
||||||
property int _dataVersion: 0
|
property int _dataVersion: 0
|
||||||
|
property string _pendingSavedKey: ""
|
||||||
|
|
||||||
readonly property var categoryOrder: Actions.getCategoryOrder()
|
readonly property var categoryOrder: Actions.getCategoryOrder()
|
||||||
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
||||||
@@ -278,7 +279,7 @@ Singleton {
|
|||||||
script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.kdl" && cp "${mainConfigPath}" "${backupPath}" && echo 'include "dms/binds.kdl"' >> "${mainConfigPath}"`;
|
script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.kdl" && cp "${mainConfigPath}" "${backupPath}" && echo 'include "dms/binds.kdl"' >> "${mainConfigPath}"`;
|
||||||
break;
|
break;
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.conf" && cp "${mainConfigPath}" "${backupPath}" && echo 'source = dms/binds.conf' >> "${mainConfigPath}"`;
|
script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.conf" && cp "${mainConfigPath}" "${backupPath}" && echo 'source = ./dms/binds.conf' >> "${mainConfigPath}"`;
|
||||||
break;
|
break;
|
||||||
case "mangowc":
|
case "mangowc":
|
||||||
script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.conf" && cp "${mainConfigPath}" "${backupPath}" && echo 'source = ./dms/binds.conf' >> "${mainConfigPath}"`;
|
script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.conf" && cp "${mainConfigPath}" "${backupPath}" && echo 'source = ./dms/binds.conf' >> "${mainConfigPath}"`;
|
||||||
@@ -342,6 +343,10 @@ Singleton {
|
|||||||
displayList = [];
|
displayList = [];
|
||||||
_dataVersion++;
|
_dataVersion++;
|
||||||
bindsLoaded();
|
bindsLoaded();
|
||||||
|
if (_pendingSavedKey) {
|
||||||
|
bindSaved(_pendingSavedKey);
|
||||||
|
_pendingSavedKey = "";
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +383,8 @@ Singleton {
|
|||||||
"key": bind.key || "",
|
"key": bind.key || "",
|
||||||
"source": bind.source || "config",
|
"source": bind.source || "config",
|
||||||
"isOverride": bind.source === "dms",
|
"isOverride": bind.source === "dms",
|
||||||
"cooldownMs": bind.cooldownMs || 0
|
"cooldownMs": bind.cooldownMs || 0,
|
||||||
|
"flags": bind.flags || ""
|
||||||
};
|
};
|
||||||
if (actionMap[action]) {
|
if (actionMap[action]) {
|
||||||
actionMap[action].keys.push(keyData);
|
actionMap[action].keys.push(keyData);
|
||||||
@@ -425,6 +431,10 @@ Singleton {
|
|||||||
displayList = list;
|
displayList = list;
|
||||||
_dataVersion++;
|
_dataVersion++;
|
||||||
bindsLoaded();
|
bindsLoaded();
|
||||||
|
if (_pendingSavedKey) {
|
||||||
|
bindSaved(_pendingSavedKey);
|
||||||
|
_pendingSavedKey = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCategories() {
|
function getCategories() {
|
||||||
@@ -444,9 +454,11 @@ Singleton {
|
|||||||
cmd.push("--replace-key", originalKey);
|
cmd.push("--replace-key", originalKey);
|
||||||
if (bindData.cooldownMs > 0)
|
if (bindData.cooldownMs > 0)
|
||||||
cmd.push("--cooldown-ms", String(bindData.cooldownMs));
|
cmd.push("--cooldown-ms", String(bindData.cooldownMs));
|
||||||
|
if (bindData.flags)
|
||||||
|
cmd.push("--flags", bindData.flags);
|
||||||
saveProcess.command = cmd;
|
saveProcess.command = cmd;
|
||||||
saveProcess.running = true;
|
saveProcess.running = true;
|
||||||
bindSaved(bindData.key);
|
_pendingSavedKey = bindData.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeBind(key) {
|
function removeBind(key) {
|
||||||
|
|||||||
@@ -415,8 +415,12 @@ Singleton {
|
|||||||
notificationModal?.close();
|
notificationModal?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showWifiPasswordModal() {
|
function showWifiPasswordModal(ssid) {
|
||||||
wifiPasswordModal?.show();
|
wifiPasswordModal?.show(ssid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHiddenNetworkModal() {
|
||||||
|
wifiPasswordModal?.showHidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideWifiPasswordModal() {
|
function hideWifiPasswordModal() {
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
importError = response.result.error || "Import failed";
|
importError = response.result.error || "Import failed";
|
||||||
ToastService.showError(importError);
|
ToastService.showError(I18n.tr("Failed to import VPN"), importError);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ Item {
|
|||||||
property string editAction: ""
|
property string editAction: ""
|
||||||
property string editDesc: ""
|
property string editDesc: ""
|
||||||
property int editCooldownMs: 0
|
property int editCooldownMs: 0
|
||||||
|
property string editFlags: ""
|
||||||
property int _savedCooldownMs: -1
|
property int _savedCooldownMs: -1
|
||||||
|
property string _savedFlags: ""
|
||||||
property bool hasChanges: false
|
property bool hasChanges: false
|
||||||
property string _actionType: ""
|
property string _actionType: ""
|
||||||
property bool addingNewKey: false
|
property bool addingNewKey: false
|
||||||
@@ -104,6 +106,12 @@ Item {
|
|||||||
} else {
|
} else {
|
||||||
editCooldownMs = keys[i].cooldownMs || 0;
|
editCooldownMs = keys[i].cooldownMs || 0;
|
||||||
}
|
}
|
||||||
|
if (_savedFlags) {
|
||||||
|
editFlags = _savedFlags;
|
||||||
|
_savedFlags = "";
|
||||||
|
} else {
|
||||||
|
editFlags = keys[i].flags || "";
|
||||||
|
}
|
||||||
hasChanges = false;
|
hasChanges = false;
|
||||||
_actionType = Actions.getActionType(editAction);
|
_actionType = Actions.getActionType(editAction);
|
||||||
useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction);
|
useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction);
|
||||||
@@ -124,6 +132,7 @@ Item {
|
|||||||
editAction = bindData.action || "";
|
editAction = bindData.action || "";
|
||||||
editDesc = bindData.desc || "";
|
editDesc = bindData.desc || "";
|
||||||
editCooldownMs = editingKeyIndex >= 0 ? (keys[editingKeyIndex].cooldownMs || 0) : 0;
|
editCooldownMs = editingKeyIndex >= 0 ? (keys[editingKeyIndex].cooldownMs || 0) : 0;
|
||||||
|
editFlags = editingKeyIndex >= 0 ? (keys[editingKeyIndex].flags || "") : "";
|
||||||
hasChanges = false;
|
hasChanges = false;
|
||||||
_actionType = Actions.getActionType(editAction);
|
_actionType = Actions.getActionType(editAction);
|
||||||
useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction);
|
useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction);
|
||||||
@@ -143,6 +152,7 @@ Item {
|
|||||||
editingKeyIndex = index;
|
editingKeyIndex = index;
|
||||||
editKey = keys[index].key;
|
editKey = keys[index].key;
|
||||||
editCooldownMs = keys[index].cooldownMs || 0;
|
editCooldownMs = keys[index].cooldownMs || 0;
|
||||||
|
editFlags = keys[index].flags || "";
|
||||||
hasChanges = false;
|
hasChanges = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,9 +165,12 @@ Item {
|
|||||||
editDesc = changes.desc;
|
editDesc = changes.desc;
|
||||||
if (changes.cooldownMs !== undefined)
|
if (changes.cooldownMs !== undefined)
|
||||||
editCooldownMs = changes.cooldownMs;
|
editCooldownMs = changes.cooldownMs;
|
||||||
|
if (changes.flags !== undefined)
|
||||||
|
editFlags = changes.flags;
|
||||||
const origKey = editingKeyIndex >= 0 && editingKeyIndex < keys.length ? keys[editingKeyIndex].key : "";
|
const origKey = editingKeyIndex >= 0 && editingKeyIndex < keys.length ? keys[editingKeyIndex].key : "";
|
||||||
const origCooldown = editingKeyIndex >= 0 && editingKeyIndex < keys.length ? (keys[editingKeyIndex].cooldownMs || 0) : 0;
|
const origCooldown = editingKeyIndex >= 0 && editingKeyIndex < keys.length ? (keys[editingKeyIndex].cooldownMs || 0) : 0;
|
||||||
hasChanges = editKey !== origKey || editAction !== (bindData.action || "") || editDesc !== (bindData.desc || "") || editCooldownMs !== origCooldown;
|
const origFlags = editingKeyIndex >= 0 && editingKeyIndex < keys.length ? (keys[editingKeyIndex].flags || "") : "";
|
||||||
|
hasChanges = editKey !== origKey || editAction !== (bindData.action || "") || editDesc !== (bindData.desc || "") || editCooldownMs !== origCooldown || editFlags !== origFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
function canSave() {
|
function canSave() {
|
||||||
@@ -176,11 +189,13 @@ Item {
|
|||||||
if (expandedLoader.item?.currentTitle !== undefined)
|
if (expandedLoader.item?.currentTitle !== undefined)
|
||||||
desc = expandedLoader.item.currentTitle;
|
desc = expandedLoader.item.currentTitle;
|
||||||
_savedCooldownMs = editCooldownMs;
|
_savedCooldownMs = editCooldownMs;
|
||||||
|
_savedFlags = editFlags;
|
||||||
saveBind(origKey, {
|
saveBind(origKey, {
|
||||||
"key": editKey,
|
"key": editKey,
|
||||||
"action": editAction,
|
"action": editAction,
|
||||||
"desc": desc,
|
"desc": desc,
|
||||||
"cooldownMs": editCooldownMs
|
"cooldownMs": editCooldownMs,
|
||||||
|
"flags": editFlags
|
||||||
});
|
});
|
||||||
hasChanges = false;
|
hasChanges = false;
|
||||||
addingNewKey = false;
|
addingNewKey = false;
|
||||||
@@ -1451,6 +1466,113 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
visible: KeybindsService.currentProvider === "hyprland"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Flags")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
Layout.preferredWidth: root._labelWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
checked: root.editFlags.indexOf("e") !== -1
|
||||||
|
onToggled: newChecked => {
|
||||||
|
let flags = root.editFlags.split("").filter(f => f !== "e");
|
||||||
|
if (newChecked)
|
||||||
|
flags.push("e");
|
||||||
|
root.updateEdit({
|
||||||
|
"flags": flags.join("")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Repeat")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
checked: root.editFlags.indexOf("l") !== -1
|
||||||
|
onToggled: newChecked => {
|
||||||
|
let flags = root.editFlags.split("").filter(f => f !== "l");
|
||||||
|
if (newChecked)
|
||||||
|
flags.push("l");
|
||||||
|
root.updateEdit({
|
||||||
|
"flags": flags.join("")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Locked")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
checked: root.editFlags.indexOf("r") !== -1
|
||||||
|
onToggled: newChecked => {
|
||||||
|
let flags = root.editFlags.split("").filter(f => f !== "r");
|
||||||
|
if (newChecked)
|
||||||
|
flags.push("r");
|
||||||
|
root.updateEdit({
|
||||||
|
"flags": flags.join("")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Release")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
checked: root.editFlags.indexOf("o") !== -1
|
||||||
|
onToggled: newChecked => {
|
||||||
|
let flags = root.editFlags.split("").filter(f => f !== "o");
|
||||||
|
if (newChecked)
|
||||||
|
flags.push("o");
|
||||||
|
root.updateEdit({
|
||||||
|
"flags": flags.join("")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Long press")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|||||||
Reference in New Issue
Block a user