mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 13:32:50 -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,
|
||||
clipboardCmd,
|
||||
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().Bool("no-repeat", false, "Disable key repeat")
|
||||
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(keybindsShowCmd)
|
||||
@@ -211,6 +212,9 @@ func runKeybindsSet(cmd *cobra.Command, args []string) {
|
||||
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
||||
options["repeat"] = false
|
||||
}
|
||||
if v, _ := cmd.Flags().GetString("flags"); v != "" {
|
||||
options["flags"] = v
|
||||
}
|
||||
|
||||
desc, _ := cmd.Flags().GetString("desc")
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
return result, result.Error
|
||||
}
|
||||
@@ -553,13 +553,14 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string) error {
|
||||
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalCommand string) error {
|
||||
configs := []struct {
|
||||
name string
|
||||
content string
|
||||
}{
|
||||
{"colors.conf", HyprColorsConfig},
|
||||
{"layout.conf", HyprLayoutConfig},
|
||||
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||
{"outputs.conf", ""},
|
||||
{"cursor.conf", ""},
|
||||
}
|
||||
|
||||
@@ -408,7 +408,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
|
||||
content, err := os.ReadFile(result.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
||||
assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty")
|
||||
assert.Contains(t, string(content), "source = ./dms/binds.conf")
|
||||
assert.Contains(t, string(content), "exec-once = ")
|
||||
})
|
||||
|
||||
@@ -444,7 +444,7 @@ general {
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
|
||||
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
|
||||
assert.Contains(t, string(newContent), "bind = $mod, T, exec, kitty")
|
||||
assert.Contains(t, string(newContent), "source = ./dms/binds.conf")
|
||||
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, "# STARTUP APPS")
|
||||
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
||||
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
|
||||
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, {{TERMINAL_COMMAND}}")
|
||||
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
|
||||
assert.Contains(t, HyprlandConfig, "source = ./dms/binds.conf")
|
||||
}
|
||||
|
||||
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)$
|
||||
|
||||
# ==================
|
||||
# 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/outputs.conf
|
||||
source = ./dms/layout.conf
|
||||
source = ./dms/cursor.conf
|
||||
source = ./dms/binds.conf
|
||||
|
||||
@@ -10,3 +10,6 @@ var HyprColorsConfig string
|
||||
|
||||
//go:embed embedded/hypr-layout.conf
|
||||
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,
|
||||
Subcategory: subcategory,
|
||||
Source: source,
|
||||
Flags: kb.Flags,
|
||||
}
|
||||
|
||||
if source == "dms" && conflicts != nil {
|
||||
@@ -224,11 +225,20 @@ func (h *HyprlandProvider) SetBind(key, action, description string, options map[
|
||||
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)
|
||||
existingBinds[normalizedKey] = &hyprlandOverrideBind{
|
||||
Key: key,
|
||||
Action: action,
|
||||
Description: description,
|
||||
Flags: flags,
|
||||
Options: options,
|
||||
}
|
||||
|
||||
@@ -250,6 +260,7 @@ type hyprlandOverrideBind struct {
|
||||
Key string
|
||||
Action 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
|
||||
}
|
||||
|
||||
@@ -281,6 +292,11 @@ func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract flags from bind type
|
||||
bindType := strings.TrimSpace(parts[0])
|
||||
flags := extractBindFlags(bindType)
|
||||
hasDescFlag := strings.Contains(flags, "d")
|
||||
|
||||
content := strings.TrimSpace(parts[1])
|
||||
commentParts := strings.SplitN(content, "#", 2)
|
||||
bindContent := strings.TrimSpace(commentParts[0])
|
||||
@@ -290,18 +306,41 @@ func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind
|
||||
comment = strings.TrimSpace(commentParts[1])
|
||||
}
|
||||
|
||||
fields := strings.SplitN(bindContent, ",", 4)
|
||||
if len(fields) < 3 {
|
||||
// For bindd, format is: mods, key, description, dispatcher, params
|
||||
var minFields, descIndex, dispatcherIndex int
|
||||
if hasDescFlag {
|
||||
minFields = 4
|
||||
descIndex = 2
|
||||
dispatcherIndex = 3
|
||||
} else {
|
||||
minFields = 3
|
||||
dispatcherIndex = 2
|
||||
}
|
||||
|
||||
fields := strings.SplitN(bindContent, ",", minFields+2)
|
||||
if len(fields) < minFields {
|
||||
continue
|
||||
}
|
||||
|
||||
mods := strings.TrimSpace(fields[0])
|
||||
keyName := strings.TrimSpace(fields[1])
|
||||
dispatcher := strings.TrimSpace(fields[2])
|
||||
|
||||
var params string
|
||||
if len(fields) > 3 {
|
||||
params = strings.TrimSpace(fields[3])
|
||||
var dispatcher, params string
|
||||
if hasDescFlag {
|
||||
if comment == "" {
|
||||
comment = strings.TrimSpace(fields[descIndex])
|
||||
}
|
||||
dispatcher = strings.TrimSpace(fields[dispatcherIndex])
|
||||
if len(fields) > dispatcherIndex+1 {
|
||||
paramParts := fields[dispatcherIndex+1:]
|
||||
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
||||
}
|
||||
} else {
|
||||
dispatcher = strings.TrimSpace(fields[dispatcherIndex])
|
||||
if len(fields) > dispatcherIndex+1 {
|
||||
paramParts := fields[dispatcherIndex+1:]
|
||||
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
||||
}
|
||||
}
|
||||
|
||||
keyStr := h.buildKeyString(mods, keyName)
|
||||
@@ -315,6 +354,7 @@ func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind
|
||||
Key: keyStr,
|
||||
Action: action,
|
||||
Description: comment,
|
||||
Flags: flags,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,11 +431,23 @@ func (h *HyprlandProvider) writeBindLine(sb *strings.Builder, bind *hyprlandOver
|
||||
mods, key := h.parseKeyString(bind.Key)
|
||||
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(", ")
|
||||
sb.WriteString(key)
|
||||
sb.WriteString(", ")
|
||||
|
||||
// For bindd (description flag), include description before dispatcher
|
||||
if strings.Contains(bind.Flags, "d") && bind.Description != "" {
|
||||
sb.WriteString(bind.Description)
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
|
||||
sb.WriteString(dispatcher)
|
||||
|
||||
if params != "" {
|
||||
@@ -403,7 +455,8 @@ func (h *HyprlandProvider) writeBindLine(sb *strings.Builder, bind *hyprlandOver
|
||||
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(bind.Description)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ type HyprlandKeyBinding struct {
|
||||
Params string `json:"params"`
|
||||
Comment string `json:"comment"`
|
||||
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 {
|
||||
@@ -218,71 +219,7 @@ func hyprlandAutogenerateComment(dispatcher, params string) string {
|
||||
|
||||
func (p *HyprlandParser) getKeybindAtLine(lineNumber int) *HyprlandKeyBinding {
|
||||
line := p.contentLines[lineNumber]
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
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,
|
||||
}
|
||||
return p.parseBindLine(line)
|
||||
}
|
||||
|
||||
func (p *HyprlandParser) getBindsRecursive(currentContent *HyprlandSection, scope int) *HyprlandSection {
|
||||
@@ -572,6 +509,11 @@ func (p *HyprlandParser) parseBindLine(line string) *HyprlandKeyBinding {
|
||||
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]
|
||||
keyParts := strings.SplitN(keys, "#", 2)
|
||||
keys = keyParts[0]
|
||||
@@ -581,19 +523,43 @@ func (p *HyprlandParser) parseBindLine(line string) *HyprlandKeyBinding {
|
||||
comment = strings.TrimSpace(keyParts[1])
|
||||
}
|
||||
|
||||
keyFields := strings.SplitN(keys, ",", 5)
|
||||
if len(keyFields) < 3 {
|
||||
// For bindd, the format is: bindd = MODS, key, description, dispatcher, params
|
||||
// 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
|
||||
}
|
||||
|
||||
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, ","))
|
||||
var dispatcher, params string
|
||||
if hasDescFlag {
|
||||
// bindd format: description is in the bind itself
|
||||
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) {
|
||||
@@ -631,9 +597,20 @@ func (p *HyprlandParser) parseBindLine(line string) *HyprlandKeyBinding {
|
||||
Dispatcher: dispatcher,
|
||||
Params: params,
|
||||
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) {
|
||||
parser := NewHyprlandParser(path)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -293,9 +301,15 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
|
||||
continue
|
||||
}
|
||||
keyStr := parser.formatBindKey(kb)
|
||||
|
||||
action := n.buildActionFromNode(child)
|
||||
if action == "" {
|
||||
action = n.formatRawAction(kb.Action, kb.Args)
|
||||
}
|
||||
|
||||
binds[keyStr] = &overrideBind{
|
||||
Key: keyStr,
|
||||
Action: n.formatRawAction(kb.Action, kb.Args),
|
||||
Action: action,
|
||||
Description: kb.Description,
|
||||
Options: n.extractOptions(child),
|
||||
}
|
||||
@@ -305,6 +319,36 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
|
||||
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 {
|
||||
if node.Properties == nil {
|
||||
return make(map[string]any)
|
||||
|
||||
@@ -121,6 +121,8 @@ func TestNiriFormatRawAction(t *testing.T) {
|
||||
}{
|
||||
{"spawn", []string{"kitty"}, "spawn kitty"},
|
||||
{"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"},
|
||||
{"fullscreen-window", nil, "fullscreen-window"},
|
||||
{"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) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||
|
||||
@@ -8,6 +8,7 @@ type Keybind struct {
|
||||
Source string `json:"source,omitempty"`
|
||||
HideOnOverlay bool `json:"hideOnOverlay,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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package network
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"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) {
|
||||
vpnTypes := []string{"openvpn", "wireguard", "vpnc", "pptp", "l2tp", "openconnect", "strongswan"}
|
||||
|
||||
var output []byte
|
||||
var err error
|
||||
var allErrors []error
|
||||
var outputStr string
|
||||
for _, vpnType := range vpnTypes {
|
||||
args := []string{"connection", "import", "type", vpnType, "file", filePath}
|
||||
cmd := exec.Command("nmcli", args...)
|
||||
output, err = cmd.CombinedOutput()
|
||||
cmd := exec.Command("nmcli", "connection", "import", "type", vpnType, "file", filePath)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
outputStr = string(output)
|
||||
break
|
||||
}
|
||||
allErrors = append(allErrors, fmt.Errorf("%s: %s", vpnType, strings.TrimSpace(string(output))))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if len(allErrors) == len(vpnTypes) {
|
||||
return &VPNImportResult{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("import failed: %s", strings.TrimSpace(string(output))),
|
||||
Error: errors.Join(allErrors...).Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
outputStr := string(output)
|
||||
var connUUID, connName string
|
||||
|
||||
lines := strings.Split(outputStr, "\n")
|
||||
|
||||
@@ -357,31 +357,51 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
||||
|
||||
savedSSIDs := make(map[string]bool)
|
||||
autoconnectMap := make(map[string]bool)
|
||||
hiddenSSIDs := make(map[string]bool)
|
||||
for _, conn := range connections {
|
||||
connSettings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if connMeta, ok := connSettings["connection"]; ok {
|
||||
if connType, ok := connMeta["type"].(string); ok && connType == "802-11-wireless" {
|
||||
if wifiSettings, ok := connSettings["802-11-wireless"]; ok {
|
||||
if ssidBytes, ok := wifiSettings["ssid"].([]byte); ok {
|
||||
ssid := string(ssidBytes)
|
||||
savedSSIDs[ssid] = true
|
||||
autoconnect := true
|
||||
if ac, ok := connMeta["autoconnect"].(bool); ok {
|
||||
autoconnect = ac
|
||||
}
|
||||
autoconnectMap[ssid] = autoconnect
|
||||
}
|
||||
}
|
||||
}
|
||||
connMeta, ok := connSettings["connection"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
connType, ok := connMeta["type"].(string)
|
||||
if !ok || connType != "802-11-wireless" {
|
||||
continue
|
||||
}
|
||||
|
||||
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()
|
||||
currentSSID := b.state.WiFiSSID
|
||||
wifiConnected := b.state.WiFiConnected
|
||||
wifiSignal := b.state.WiFiSignal
|
||||
wifiBSSID := b.state.WiFiBSSID
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
seenSSIDs := make(map[string]*WiFiNetwork)
|
||||
@@ -444,6 +464,7 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
||||
Connected: ssid == currentSSID,
|
||||
Saved: savedSSIDs[ssid],
|
||||
Autoconnect: autoconnectMap[ssid],
|
||||
Hidden: hiddenSSIDs[ssid],
|
||||
Frequency: freq,
|
||||
Mode: modeStr,
|
||||
Rate: maxBitrate / 1000,
|
||||
@@ -454,6 +475,23 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
||||
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)
|
||||
|
||||
b.stateMutex.Lock()
|
||||
@@ -515,40 +553,53 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
dev := devInfo.device
|
||||
w := devInfo.wireless
|
||||
apPaths, err := w.GetAccessPoints()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get access points: %w", err)
|
||||
}
|
||||
|
||||
var targetAP gonetworkmanager.AccessPoint
|
||||
for _, ap := range apPaths {
|
||||
ssid, err := ap.GetPropertySSID()
|
||||
if err != nil || ssid != req.SSID {
|
||||
continue
|
||||
var flags, wpaFlags, rsnFlags uint32
|
||||
|
||||
if !req.Hidden {
|
||||
apPaths, err := w.GetAccessPoints()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get access points: %w", err)
|
||||
}
|
||||
targetAP = ap
|
||||
break
|
||||
}
|
||||
|
||||
if targetAP == nil {
|
||||
return fmt.Errorf("access point not found: %s", req.SSID)
|
||||
}
|
||||
for _, ap := range apPaths {
|
||||
ssid, err := ap.GetPropertySSID()
|
||||
if err != nil || ssid != req.SSID {
|
||||
continue
|
||||
}
|
||||
targetAP = ap
|
||||
break
|
||||
}
|
||||
|
||||
flags, _ := targetAP.GetPropertyFlags()
|
||||
wpaFlags, _ := targetAP.GetPropertyWPAFlags()
|
||||
rsnFlags, _ := targetAP.GetPropertyRSNFlags()
|
||||
if targetAP == nil {
|
||||
return fmt.Errorf("access point not found: %s", req.SSID)
|
||||
}
|
||||
|
||||
flags, _ = targetAP.GetPropertyFlags()
|
||||
wpaFlags, _ = targetAP.GetPropertyWPAFlags()
|
||||
rsnFlags, _ = targetAP.GetPropertyRSNFlags()
|
||||
}
|
||||
|
||||
const KeyMgmt8021x = uint32(512)
|
||||
const KeyMgmtPsk = uint32(256)
|
||||
const KeyMgmtSae = uint32(1024)
|
||||
|
||||
isEnterprise := (wpaFlags&KeyMgmt8021x) != 0 || (rsnFlags&KeyMgmt8021x) != 0
|
||||
isPsk := (wpaFlags&KeyMgmtPsk) != 0 || (rsnFlags&KeyMgmtPsk) != 0
|
||||
isSae := (wpaFlags&KeyMgmtSae) != 0 || (rsnFlags&KeyMgmtSae) != 0
|
||||
var isEnterprise, isPsk, isSae, secured bool
|
||||
|
||||
secured := flags != uint32(gonetworkmanager.Nm80211APFlagsNone) ||
|
||||
wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) ||
|
||||
rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone)
|
||||
switch {
|
||||
case req.Hidden:
|
||||
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 {
|
||||
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"}
|
||||
|
||||
if secured {
|
||||
settings["802-11-wireless"] = map[string]any{
|
||||
wifiSettings := map[string]any{
|
||||
"ssid": []byte(req.SSID),
|
||||
"mode": "infrastructure",
|
||||
"security": "802-11-wireless-security",
|
||||
}
|
||||
if req.Hidden {
|
||||
wifiSettings["hidden"] = true
|
||||
}
|
||||
settings["802-11-wireless"] = wifiSettings
|
||||
|
||||
switch {
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
settings["802-11-wireless"] = map[string]any{
|
||||
wifiSettings := map[string]any{
|
||||
"ssid": []byte(req.SSID),
|
||||
"mode": "infrastructure",
|
||||
}
|
||||
if req.Hidden {
|
||||
wifiSettings["hidden"] = true
|
||||
}
|
||||
settings["802-11-wireless"] = wifiSettings
|
||||
}
|
||||
|
||||
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)")
|
||||
}
|
||||
|
||||
_, 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 {
|
||||
return fmt.Errorf("failed to activate connection: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("[createAndConnectWiFi] Connection activation initiated, waiting for NetworkManager state changes...")
|
||||
} 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 {
|
||||
return fmt.Errorf("failed to connect: %w", err)
|
||||
}
|
||||
@@ -813,6 +881,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
||||
|
||||
savedSSIDs := make(map[string]bool)
|
||||
autoconnectMap := make(map[string]bool)
|
||||
hiddenSSIDs := make(map[string]bool)
|
||||
for _, conn := range connections {
|
||||
connSettings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
@@ -846,6 +915,10 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
||||
autoconnect = ac
|
||||
}
|
||||
autoconnectMap[ssid] = autoconnect
|
||||
|
||||
if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden {
|
||||
hiddenSSIDs[ssid] = true
|
||||
}
|
||||
}
|
||||
|
||||
var devices []WiFiDevice
|
||||
@@ -939,6 +1012,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
||||
Connected: connected && apSSID == ssid,
|
||||
Saved: savedSSIDs[apSSID],
|
||||
Autoconnect: autoconnectMap[apSSID],
|
||||
Hidden: hiddenSSIDs[apSSID],
|
||||
Frequency: freq,
|
||||
Mode: modeStr,
|
||||
Rate: maxBitrate / 1000,
|
||||
@@ -949,6 +1023,25 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
||||
seenSSIDs[apSSID] = &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)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ type WiFiNetwork struct {
|
||||
Connected bool `json:"connected"`
|
||||
Saved bool `json:"saved"`
|
||||
Autoconnect bool `json:"autoconnect"`
|
||||
Hidden bool `json:"hidden"`
|
||||
Frequency uint32 `json:"frequency"`
|
||||
Mode string `json:"mode"`
|
||||
Rate uint32 `json:"rate"`
|
||||
@@ -127,6 +128,7 @@ type ConnectionRequest struct {
|
||||
AnonymousIdentity string `json:"anonymousIdentity,omitempty"`
|
||||
DomainSuffixMatch string `json:"domainSuffixMatch,omitempty"`
|
||||
Interactive bool `json:"interactive,omitempty"`
|
||||
Hidden bool `json:"hidden,omitempty"`
|
||||
Device string `json:"device,omitempty"`
|
||||
EAPMethod string `json:"eapMethod,omitempty"`
|
||||
Phase2Auth string `json:"phase2Auth,omitempty"`
|
||||
|
||||
@@ -46,7 +46,9 @@ const KEY_MAP = {
|
||||
16777349: "XF86AudioMedia",
|
||||
16777350: "XF86AudioRecord",
|
||||
16842798: "XF86MonBrightnessUp",
|
||||
16777394: "XF86MonBrightnessUp",
|
||||
16842797: "XF86MonBrightnessDown",
|
||||
16777395: "XF86MonBrightnessDown",
|
||||
16842800: "XF86KbdBrightnessUp",
|
||||
16842799: "XF86KbdBrightnessDown",
|
||||
16842796: "XF86PowerOff",
|
||||
|
||||
@@ -1587,6 +1587,9 @@ Singleton {
|
||||
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() {
|
||||
updateXResources();
|
||||
if (typeof CompositorService === "undefined")
|
||||
|
||||
@@ -2,6 +2,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals
|
||||
import qs.Modals.Changelog
|
||||
import qs.Modals.Clipboard
|
||||
import qs.Modals.Greeter
|
||||
import qs.Modals.Settings
|
||||
@@ -836,9 +837,29 @@ Item {
|
||||
function onGreeterRequested() {
|
||||
if (greeterLoader.active && greeterLoader.item) {
|
||||
greeterLoader.item.show();
|
||||
} else {
|
||||
greeterLoader.active = true;
|
||||
return;
|
||||
}
|
||||
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 description: ""
|
||||
|
||||
signal clicked
|
||||
|
||||
readonly property real iconContainerSize: Math.round(Theme.iconSize * 1.5)
|
||||
|
||||
height: Math.round(Theme.fontSizeMedium * 6.4)
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
opacity: mouseArea.containsMouse ? 0.12 : 0
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
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.Effects
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
@@ -87,6 +89,7 @@ Item {
|
||||
iconName: "auto_awesome"
|
||||
title: I18n.tr("Dynamic Theming", "greeter feature card title")
|
||||
description: I18n.tr("Colors from wallpaper", "greeter feature card description")
|
||||
onClicked: PopoutService.openSettingsWithTab("theme")
|
||||
}
|
||||
|
||||
GreeterFeatureCard {
|
||||
@@ -94,6 +97,7 @@ Item {
|
||||
iconName: "format_paint"
|
||||
title: I18n.tr("App Theming", "greeter feature card title")
|
||||
description: I18n.tr("GTK, Qt, IDEs, more", "greeter feature card description")
|
||||
onClicked: PopoutService.openSettingsWithTab("theme")
|
||||
}
|
||||
|
||||
GreeterFeatureCard {
|
||||
@@ -101,6 +105,7 @@ Item {
|
||||
iconName: "download"
|
||||
title: I18n.tr("Theme Registry", "greeter feature card title")
|
||||
description: I18n.tr("Community themes", "greeter feature card description")
|
||||
onClicked: PopoutService.openSettingsWithTab("theme")
|
||||
}
|
||||
|
||||
GreeterFeatureCard {
|
||||
@@ -108,6 +113,7 @@ Item {
|
||||
iconName: "view_carousel"
|
||||
title: I18n.tr("DankBar", "greeter feature card title")
|
||||
description: I18n.tr("Modular widget bar", "greeter feature card description")
|
||||
onClicked: PopoutService.openSettingsWithTab("dankbar_settings")
|
||||
}
|
||||
|
||||
GreeterFeatureCard {
|
||||
@@ -115,6 +121,7 @@ Item {
|
||||
iconName: "extension"
|
||||
title: I18n.tr("Plugins", "greeter feature card title")
|
||||
description: I18n.tr("Extensible architecture", "greeter feature card description")
|
||||
onClicked: PopoutService.openSettingsWithTab("plugins")
|
||||
}
|
||||
|
||||
GreeterFeatureCard {
|
||||
@@ -122,6 +129,10 @@ Item {
|
||||
iconName: "layers"
|
||||
title: I18n.tr("Multi-Monitor", "greeter feature card title")
|
||||
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 {
|
||||
@@ -129,6 +140,7 @@ Item {
|
||||
iconName: "nightlight"
|
||||
title: I18n.tr("Display Control", "greeter feature card title")
|
||||
description: I18n.tr("Night mode & gamma", "greeter feature card description")
|
||||
onClicked: PopoutService.openSettingsWithTab("display_gamma")
|
||||
}
|
||||
|
||||
GreeterFeatureCard {
|
||||
@@ -136,13 +148,16 @@ Item {
|
||||
iconName: "tune"
|
||||
title: I18n.tr("Control Center", "greeter feature card title")
|
||||
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 {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
iconName: "density_small"
|
||||
title: I18n.tr("System Tray", "greeter feature card title")
|
||||
description: I18n.tr("Background app icons", "greeter feature card description")
|
||||
iconName: "lock"
|
||||
title: I18n.tr("Lock Screen", "greeter feature card title")
|
||||
description: I18n.tr("Security & privacy", "greeter feature card description")
|
||||
onClicked: PopoutService.openSettingsWithTab("lock_screen")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,15 +34,19 @@ FloatingWindow {
|
||||
}
|
||||
|
||||
function showWithTab(tabIndex: int) {
|
||||
if (tabIndex >= 0)
|
||||
if (tabIndex >= 0) {
|
||||
currentTabIndex = tabIndex;
|
||||
sidebar.autoExpandForTab(tabIndex);
|
||||
}
|
||||
visible = true;
|
||||
}
|
||||
|
||||
function showWithTabName(tabName: string) {
|
||||
var idx = sidebar.resolveTabIndex(tabName);
|
||||
if (idx >= 0)
|
||||
if (idx >= 0) {
|
||||
currentTabIndex = idx;
|
||||
sidebar.autoExpandForTab(idx);
|
||||
}
|
||||
visible = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ FloatingWindow {
|
||||
property string wifiPasswordInput: ""
|
||||
property string wifiUsernameInput: ""
|
||||
property bool requiresEnterprise: false
|
||||
property bool isHiddenNetwork: false
|
||||
|
||||
property string wifiAnonymousIdentityInput: ""
|
||||
property string wifiDomainInput: ""
|
||||
@@ -44,6 +45,8 @@ FloatingWindow {
|
||||
property int calculatedHeight: {
|
||||
let h = headerHeight + buttonRowHeight + Theme.spacingL * 2;
|
||||
h += fieldsInfo.length * inputFieldWithSpacing;
|
||||
if (isHiddenNetwork)
|
||||
h += inputFieldWithSpacing;
|
||||
if (showUsernameField)
|
||||
h += inputFieldWithSpacing;
|
||||
if (showPasswordField)
|
||||
@@ -68,6 +71,10 @@ FloatingWindow {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isHiddenNetwork) {
|
||||
ssidInput.forceActiveFocus();
|
||||
return;
|
||||
}
|
||||
if (requiresEnterprise && !isVpnPrompt) {
|
||||
usernameInput.forceActiveFocus();
|
||||
return;
|
||||
@@ -82,6 +89,7 @@ FloatingWindow {
|
||||
wifiAnonymousIdentityInput = "";
|
||||
wifiDomainInput = "";
|
||||
isPromptMode = false;
|
||||
isHiddenNetwork = false;
|
||||
promptToken = "";
|
||||
promptReason = "";
|
||||
promptFields = [];
|
||||
@@ -100,6 +108,30 @@ FloatingWindow {
|
||||
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) {
|
||||
isPromptMode = true;
|
||||
promptToken = token;
|
||||
@@ -184,8 +216,9 @@ FloatingWindow {
|
||||
}
|
||||
NetworkService.submitCredentials(promptToken, secrets, savePasswordCheckbox.checked);
|
||||
} else {
|
||||
const ssid = isHiddenNetwork ? ssidInput.text : wifiPasswordSSID;
|
||||
const username = requiresEnterprise ? usernameInput.text : "";
|
||||
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username, wifiAnonymousIdentityInput, wifiDomainInput);
|
||||
NetworkService.connectToWifi(ssid, passwordInput.text, username, wifiAnonymousIdentityInput, wifiDomainInput, isHiddenNetwork);
|
||||
}
|
||||
|
||||
hide();
|
||||
@@ -196,6 +229,8 @@ FloatingWindow {
|
||||
passwordInput.text = "";
|
||||
if (requiresEnterprise)
|
||||
usernameInput.text = "";
|
||||
if (isHiddenNetwork)
|
||||
ssidInput.text = "";
|
||||
}
|
||||
|
||||
function clearAndClose() {
|
||||
@@ -215,6 +250,8 @@ FloatingWindow {
|
||||
return I18n.tr("Smartcard PIN");
|
||||
if (isVpnPrompt)
|
||||
return I18n.tr("VPN Password");
|
||||
if (isHiddenNetwork)
|
||||
return I18n.tr("Hidden Network");
|
||||
return I18n.tr("Wi-Fi Password");
|
||||
}
|
||||
minimumSize: Qt.size(420, calculatedHeight)
|
||||
@@ -236,6 +273,7 @@ FloatingWindow {
|
||||
usernameInput.text = "";
|
||||
anonInput.text = "";
|
||||
domainMatchInput.text = "";
|
||||
ssidInput.text = "";
|
||||
for (var i = 0; i < dynamicFieldsRepeater.count; i++) {
|
||||
const item = dynamicFieldsRepeater.itemAt(i);
|
||||
if (item?.children[0])
|
||||
@@ -296,6 +334,8 @@ FloatingWindow {
|
||||
return I18n.tr("Smartcard Authentication");
|
||||
if (isVpnPrompt)
|
||||
return I18n.tr("Connect to VPN");
|
||||
if (isHiddenNetwork)
|
||||
return I18n.tr("Connect to Hidden Network");
|
||||
return I18n.tr("Connect to Wi-Fi");
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
@@ -315,6 +355,8 @@ FloatingWindow {
|
||||
return I18n.tr("Enter credentials for ") + wifiPasswordSSID;
|
||||
if (isVpnPrompt)
|
||||
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 ");
|
||||
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 {
|
||||
id: dynamicFieldsRepeater
|
||||
model: fieldsInfo
|
||||
@@ -696,6 +766,8 @@ FloatingWindow {
|
||||
}
|
||||
if (isVpnPrompt)
|
||||
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;
|
||||
}
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
@@ -401,8 +401,8 @@ Singleton {
|
||||
}
|
||||
|
||||
function checkIncludeStatus() {
|
||||
const paths = getConfigPaths();
|
||||
if (!paths) {
|
||||
const compositor = CompositorService.compositor;
|
||||
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") {
|
||||
includeStatus = {
|
||||
"exists": false,
|
||||
"included": false
|
||||
@@ -410,14 +410,27 @@ Singleton {
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = (compositor === "niri") ? "outputs.kdl" : "outputs.conf";
|
||||
const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
|
||||
|
||||
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;
|
||||
const parts = output.trim().split(" ");
|
||||
includeStatus = {
|
||||
"exists": parts[0] === "true",
|
||||
"included": parts[1] === "true"
|
||||
};
|
||||
if (exitCode !== 0) {
|
||||
includeStatus = {
|
||||
"exists": false,
|
||||
"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.weight: Font.Medium
|
||||
color: root.isConnected ? Theme.surfaceText : Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
elide: Text.ElideMiddle
|
||||
width: Math.min(implicitWidth, root.width - 8)
|
||||
|
||||
@@ -768,6 +768,13 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankActionButton {
|
||||
iconName: "wifi_find"
|
||||
buttonSize: 32
|
||||
visible: NetworkService.backend === "networkmanager" && NetworkService.wifiEnabled && !NetworkService.wifiToggling
|
||||
onClicked: PopoutService.showHiddenNetworkModal()
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "refresh"
|
||||
buttonSize: 32
|
||||
@@ -1102,6 +1109,14 @@ Item {
|
||||
visible: isPinned
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "visibility_off"
|
||||
size: 14
|
||||
color: Theme.surfaceVariantText
|
||||
visible: modelData.hidden || false
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
@@ -1127,6 +1142,20 @@ Item {
|
||||
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 {
|
||||
text: "•"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
|
||||
@@ -55,22 +55,36 @@ Item {
|
||||
}
|
||||
|
||||
function checkCursorIncludeStatus() {
|
||||
const paths = getCursorConfigPaths();
|
||||
if (!paths) {
|
||||
const compositor = CompositorService.compositor;
|
||||
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") {
|
||||
cursorIncludeStatus = {
|
||||
"exists": false,
|
||||
"included": false
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = (compositor === "niri") ? "cursor.kdl" : "cursor.conf";
|
||||
const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
|
||||
|
||||
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;
|
||||
const parts = output.trim().split(" ");
|
||||
cursorIncludeStatus = {
|
||||
"exists": parts[0] === "true",
|
||||
"included": parts[1] === "true"
|
||||
};
|
||||
if (exitCode !== 0) {
|
||||
cursorIncludeStatus = {
|
||||
"exists": false,
|
||||
"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();
|
||||
}
|
||||
|
||||
function connectToWifi(ssid, password = "", username = "", anonymousIdentity = "", domainSuffixMatch = "") {
|
||||
function connectToWifi(ssid, password = "", username = "", anonymousIdentity = "", domainSuffixMatch = "", hidden = false) {
|
||||
if (!networkAvailable || isConnecting)
|
||||
return;
|
||||
pendingConnectionSSID = ssid;
|
||||
@@ -427,6 +427,8 @@ Singleton {
|
||||
};
|
||||
if (effectiveWifiDevice)
|
||||
params.device = effectiveWifiDevice;
|
||||
if (hidden)
|
||||
params.hidden = true;
|
||||
|
||||
if (DMSService.apiVersion >= 7) {
|
||||
if (password || username) {
|
||||
@@ -611,8 +613,8 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function connectToWifiAndSetPreference(ssid, password, username = "", anonymousIdentity = "", domainSuffixMatch = "") {
|
||||
connectToWifi(ssid, password, username, anonymousIdentity, domainSuffixMatch);
|
||||
function connectToWifiAndSetPreference(ssid, password, username = "", anonymousIdentity = "", domainSuffixMatch = "", hidden = false) {
|
||||
connectToWifi(ssid, password, username, anonymousIdentity, domainSuffixMatch, hidden);
|
||||
setNetworkPreference("wifi");
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ Singleton {
|
||||
property var _flatCache: []
|
||||
property var displayList: []
|
||||
property int _dataVersion: 0
|
||||
property string _pendingSavedKey: ""
|
||||
|
||||
readonly property var categoryOrder: Actions.getCategoryOrder()
|
||||
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}"`;
|
||||
break;
|
||||
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;
|
||||
case "mangowc":
|
||||
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 = [];
|
||||
_dataVersion++;
|
||||
bindsLoaded();
|
||||
if (_pendingSavedKey) {
|
||||
bindSaved(_pendingSavedKey);
|
||||
_pendingSavedKey = "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -378,7 +383,8 @@ Singleton {
|
||||
"key": bind.key || "",
|
||||
"source": bind.source || "config",
|
||||
"isOverride": bind.source === "dms",
|
||||
"cooldownMs": bind.cooldownMs || 0
|
||||
"cooldownMs": bind.cooldownMs || 0,
|
||||
"flags": bind.flags || ""
|
||||
};
|
||||
if (actionMap[action]) {
|
||||
actionMap[action].keys.push(keyData);
|
||||
@@ -425,6 +431,10 @@ Singleton {
|
||||
displayList = list;
|
||||
_dataVersion++;
|
||||
bindsLoaded();
|
||||
if (_pendingSavedKey) {
|
||||
bindSaved(_pendingSavedKey);
|
||||
_pendingSavedKey = "";
|
||||
}
|
||||
}
|
||||
|
||||
function getCategories() {
|
||||
@@ -444,9 +454,11 @@ Singleton {
|
||||
cmd.push("--replace-key", originalKey);
|
||||
if (bindData.cooldownMs > 0)
|
||||
cmd.push("--cooldown-ms", String(bindData.cooldownMs));
|
||||
if (bindData.flags)
|
||||
cmd.push("--flags", bindData.flags);
|
||||
saveProcess.command = cmd;
|
||||
saveProcess.running = true;
|
||||
bindSaved(bindData.key);
|
||||
_pendingSavedKey = bindData.key;
|
||||
}
|
||||
|
||||
function removeBind(key) {
|
||||
|
||||
@@ -415,8 +415,12 @@ Singleton {
|
||||
notificationModal?.close();
|
||||
}
|
||||
|
||||
function showWifiPasswordModal() {
|
||||
wifiPasswordModal?.show();
|
||||
function showWifiPasswordModal(ssid) {
|
||||
wifiPasswordModal?.show(ssid);
|
||||
}
|
||||
|
||||
function showHiddenNetworkModal() {
|
||||
wifiPasswordModal?.showHidden();
|
||||
}
|
||||
|
||||
function hideWifiPasswordModal() {
|
||||
|
||||
@@ -95,7 +95,7 @@ Singleton {
|
||||
}
|
||||
|
||||
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 editDesc: ""
|
||||
property int editCooldownMs: 0
|
||||
property string editFlags: ""
|
||||
property int _savedCooldownMs: -1
|
||||
property string _savedFlags: ""
|
||||
property bool hasChanges: false
|
||||
property string _actionType: ""
|
||||
property bool addingNewKey: false
|
||||
@@ -104,6 +106,12 @@ Item {
|
||||
} else {
|
||||
editCooldownMs = keys[i].cooldownMs || 0;
|
||||
}
|
||||
if (_savedFlags) {
|
||||
editFlags = _savedFlags;
|
||||
_savedFlags = "";
|
||||
} else {
|
||||
editFlags = keys[i].flags || "";
|
||||
}
|
||||
hasChanges = false;
|
||||
_actionType = Actions.getActionType(editAction);
|
||||
useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction);
|
||||
@@ -124,6 +132,7 @@ Item {
|
||||
editAction = bindData.action || "";
|
||||
editDesc = bindData.desc || "";
|
||||
editCooldownMs = editingKeyIndex >= 0 ? (keys[editingKeyIndex].cooldownMs || 0) : 0;
|
||||
editFlags = editingKeyIndex >= 0 ? (keys[editingKeyIndex].flags || "") : "";
|
||||
hasChanges = false;
|
||||
_actionType = Actions.getActionType(editAction);
|
||||
useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction);
|
||||
@@ -143,6 +152,7 @@ Item {
|
||||
editingKeyIndex = index;
|
||||
editKey = keys[index].key;
|
||||
editCooldownMs = keys[index].cooldownMs || 0;
|
||||
editFlags = keys[index].flags || "";
|
||||
hasChanges = false;
|
||||
}
|
||||
|
||||
@@ -155,9 +165,12 @@ Item {
|
||||
editDesc = changes.desc;
|
||||
if (changes.cooldownMs !== undefined)
|
||||
editCooldownMs = changes.cooldownMs;
|
||||
if (changes.flags !== undefined)
|
||||
editFlags = changes.flags;
|
||||
const origKey = editingKeyIndex >= 0 && editingKeyIndex < keys.length ? keys[editingKeyIndex].key : "";
|
||||
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() {
|
||||
@@ -176,11 +189,13 @@ Item {
|
||||
if (expandedLoader.item?.currentTitle !== undefined)
|
||||
desc = expandedLoader.item.currentTitle;
|
||||
_savedCooldownMs = editCooldownMs;
|
||||
_savedFlags = editFlags;
|
||||
saveBind(origKey, {
|
||||
"key": editKey,
|
||||
"action": editAction,
|
||||
"desc": desc,
|
||||
"cooldownMs": editCooldownMs
|
||||
"cooldownMs": editCooldownMs,
|
||||
"flags": editFlags
|
||||
});
|
||||
hasChanges = 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 {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Reference in New Issue
Block a user