1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-28 23:42:51 -05:00

Compare commits

..

90 Commits

Author SHA1 Message Date
Flux
a7cdb39b0b labwc patch (#1391) 2026-01-16 09:52:13 -05:00
bbedward
0ceba92a23 i18n: more RTL fixes across settings 2026-01-16 09:52:13 -05:00
bbedward
4daa7a4c88 popout: fix cross-monitor handling of widgets fixes #1364 2026-01-16 09:52:13 -05:00
bbedward
cc4a6a5899 doctor: add mango and labwc to compositors fixes #1394 2026-01-16 09:52:13 -05:00
bbedward
994947477c greeter: remove WLR_DRM_DEVICES setting fixes #1393 2026-01-16 09:52:13 -05:00
bbedward
311817ee97 dankbar: fix property preservation in widgets fixes #1392 2026-01-16 09:52:13 -05:00
bbedward
b80c73f9b9 weather: fix precipitationw weekly propability fixes #1395 2026-01-16 09:52:13 -05:00
bbedward
a85101c099 plugins: ensure daemon plugins not instantiated twice 2026-01-16 09:52:13 -05:00
bbedward
3513d57e06 cc: fixed width column, remove anchoring from individual icons on vbar maybe #1376 2026-01-16 09:52:13 -05:00
Lucas
1234847abb nix: fix home module (#1387) 2026-01-16 09:52:13 -05:00
Bailey
0ed595b43d nix: Support specifying systemd target (#1385) 2026-01-16 09:52:13 -05:00
Ivan Molodetskikh
060cbefc79 Add screencast indicator for niri (#1361)
* niri: Handle new Cast events

* bar: Add screen sharing indicator

Configurable like other icons; on by default.

* lockscreen: Add screen sharing indicator
2026-01-16 09:52:10 -05:00
bbedward
e022c04519 bump version 2026-01-15 23:45:21 -05:00
bbedward
f534384e5e cc: wrap icons in fixed size containers
maybe #1376
2026-01-15 23:45:09 -05:00
bbedward
a25cdb43d5 controlcenter: fix visibility condition of no icons fixes #1377 2026-01-15 23:45:09 -05:00
purian23
4e9b4ca400 Fix fedora version format 2026-01-15 23:44:19 -05:00
bbedward
5bab1c98b1 plugins: fix plugin confirm third part repo window 2026-01-15 23:44:19 -05:00
purian23
2284bb002f distro: Update Fedora dynamic versioning 2026-01-15 23:44:19 -05:00
purian23
b0611d6104 feat: Allow more pinned services in Control Center/Settings 2026-01-15 23:44:19 -05:00
purian23
27965862d6 core: Update ghostty on dankinstall 2026-01-15 23:44:19 -05:00
Abhinav Chalise
e74a901e05 fix volume osd sliding ui update for vertical layout (#1382) 2026-01-15 23:44:19 -05:00
bbedward
77794deb2c widgets: add fallback for steam apps 2026-01-15 23:44:19 -05:00
Lucas
1c10746e50 doctor: use dbus for checking on services (#1384)
* doctor: use dbus for checking on services

* doctor: show docs URL for failed checks

* core: remove unused function
2026-01-15 23:44:19 -05:00
bbedward
8ecb7282b9 dankdash: fix weather open IPC fixes #1367 2026-01-15 23:44:19 -05:00
bbedward
9b3fa804ab matugen: fix nvim ID in skipTemplates 2026-01-15 23:44:19 -05:00
purian23
b2ad31a27e dankdash: Center Media Art & Controls 2026-01-15 23:44:19 -05:00
bbedward
db17e4cb14 i18n: update terms 2026-01-15 23:44:02 -05:00
bbedward
1b7dcf56a8 bump VERSION 2026-01-14 08:00:15 -05:00
bbedward
502bb88e92 modals: fix wifi passowrd, polkit, and VPN import 2026-01-14 08:00:05 -05:00
bbedward
b76d0ce97d settings: fix child windows on newer quickshell-git 2026-01-13 16:58:08 -05:00
bbedward
fa66d330cf bump VERSION 2026-01-13 16:42:49 -05:00
Lucas
157eab2d07 settings: fix modal not opening on latest quickshell (#1357) 2026-01-13 16:42:38 -05:00
Lucas
f50ad2dc22 nix: escape version string (#1353) 2026-01-13 16:42:38 -05:00
bbedward
cd9d92d884 update changelog link and VERSION 2026-01-13 08:31:50 -05:00
Lucas
1b69a5e62b nix: add wtype dependency (#1346) 2026-01-13 08:27:46 -05:00
bbedward
61d311b157 widgets: fix running apps positioning and popup manager 2026-01-13 08:26:29 -05:00
bbedward
6b76b86930 notifications: remove redundant trimStored and add null safety 2026-01-12 23:37:49 -05:00
bbedward
dcfb947c36 desktop widgets: sync position across screens option, clickthrough
option, grouping in settings, repositioning, new IPCs for control
fixes #1300
fixes #1301
2026-01-12 15:31:34 -05:00
bbedward
59893b7f44 notifications: use Theme.primary to represent do not distrub in bar 2026-01-12 11:57:42 -05:00
bbedward
d2c62f5533 matugen: add support for vscode-insiders 2026-01-12 11:46:29 -05:00
bbedward
2bbe9a0c45 core/wlcontext: use infinite poll timeout 2026-01-12 11:26:35 -05:00
bbedward
4e2ce82c0a notifications: swipe to dismiss on history 2026-01-12 11:08:22 -05:00
bbedward
104762186f widgets: respect radius for inactive DankButtonGroup i tems 2026-01-12 10:26:50 -05:00
bbedward
f1233ab1e3 matugen: add post_hook for mango 2026-01-12 10:05:19 -05:00
bbedward
d6b407ec37 settings: fix wallpaper preview cache update on per-mode change 2026-01-12 09:58:58 -05:00
bbedward
022b4b4bb3 enable changelog 2026-01-12 09:46:50 -05:00
bbedward
49b322582d keybinds: fix sh, fix screenshot-window options, empty args
part of #914
2026-01-12 09:35:30 -05:00
bbedward
1280bd047d settings: fix sidebar binding when clicked by emitting signal 2026-01-11 22:43:29 -05:00
bbedward
6f206d7523 dankdash: fix 24H format in weather tab
fixes #1283
2026-01-11 21:45:28 -05:00
bbedward
2e58283859 dgop: use used mem directly from API
- conditionally because it depends on newer dgop
2026-01-11 17:32:36 -05:00
Marcus Ramberg
99a5721fe8 settings: extract tab headings for search (#1333)
* settings: extract tab headings for search

* fix pre-commit

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-01-11 17:14:45 -05:00
bbedward
5302ebd840 notifications: spacing improvements
fixes #1241
2026-01-11 14:35:34 -05:00
bbedward
fa427ea1ac settings: fix clipping of generic color selector
fixes #1242
2026-01-11 14:04:48 -05:00
bbedward
7027bd1646 systemtray: use Theme radius for menu options
fixes #1331
2026-01-11 14:03:23 -05:00
bbedward
3c38e17472 notifications: add compact mode, expansion in history, expansion in
popup
fixes #1282
2026-01-11 12:11:44 -05:00
shalevc1098
510ea5d2e4 feat: configurable app id substitutions (#1317)
* feat: add configurable app ID substitutions setting

* feat: add live icon updates when substitutions change

* fix: cursor not showing on headerActions in non-collapsible cards

* fix: address PR review feedback

- add tags for search index
- remove hardcoded height from text fields
2026-01-10 21:00:15 -05:00
bbedward
bb2234d328 cc: dont show preference flip if not on ethernet and wifi 2026-01-10 10:35:48 -05:00
bbedward
edbdeb0fb8 widgets: add artix and void NF mappings 2026-01-10 10:18:09 -05:00
Kostiantyn To
19541fc573 update-service: add Artix Linux to supported distributions list (#1318) 2026-01-10 10:18:00 -05:00
bbedward
7c936cacfb niri: fix effectiveScreenAssignment in modal 2026-01-10 10:13:41 -05:00
bbedward
c60cd3a341 modals/auth: add show password option
fixes #1311
2026-01-09 22:20:18 -05:00
shalevc1098
e37135f80d feat: map steam_app_ID to steam_icon_ID for actual game icons (#1312)
Steam Proton games use window class steam_app_XXXXX. Steam installs
icons as steam_icon_XXXXX. This maps between them so actual game
icons display instead of generic controller fallback.
2026-01-09 21:40:35 -05:00
bbedward
aac937cbcc settingns: fix missing help text on desktop widgets 2026-01-09 19:07:37 -05:00
bbedward
4b46d022af workspaces: add color options, add focus follows monitor, remove
per-monitor option (was misleading)
relevant to #1207
2026-01-09 14:10:57 -05:00
bbedward
7f0181b310 matugen/vscode: fix selection contrast 2026-01-09 10:16:03 -05:00
bbedward
6a109274f8 hyprland: always use single window 2026-01-09 09:57:31 -05:00
bbedward
0f09cc693a lock: handle case where session lock is rejected 2026-01-09 09:46:39 -05:00
bbedward
af0166a553 dankbar: add bar get/setPosition IPC 2026-01-09 00:09:49 -05:00
bbedward
a283017f26 audio: recreate media players on pipewire device change 2026-01-08 23:35:42 -05:00
bbedward
5ae2cd1dfb i18n: fix RTL in plugin settings 2026-01-08 19:16:55 -05:00
bbedward
eece811fb0 i18n: more RTL repairs 2026-01-08 18:45:38 -05:00
bbedward
1ff1f3a7f2 i18n: more RTL layout enhancements 2026-01-08 16:11:30 -05:00
bbedward
a21a846bf5 wallpaper: encode image URIs
fixes #1306
2026-01-08 14:32:12 -05:00
Anton Kesy
f5f21e738a fix typos (#1304) 2026-01-08 14:10:24 -05:00
bbedward
033e62418a hyprland: fix cursor setting 2026-01-08 09:30:52 -05:00
bbedward
3c69e8b1cc revert readme 2026-01-07 22:59:28 -05:00
bbedward
118be27796 update readme 2026-01-07 22:56:16 -05:00
bbedward
721d35d417 readme:update vid url 2026-01-07 22:54:38 -05:00
bbedward
7bc3d5910d settings: fade to lock and monitor off by default on 2026-01-07 21:31:12 -05:00
bbedward
ccc7047be0 welcome: make the first page stuff clickable
fixes #1295
2026-01-07 21:22:15 -05:00
bbedward
a5e107c89d changelog: capability to display new release message 2026-01-07 20:15:50 -05:00
bbedward
646d60dcbf displays: fix text-alignment in model mode 2026-01-07 16:54:31 -05:00
bbedward
5dc7c0d797 core: add resolve-include recursive
fixes #1294
2026-01-07 16:45:31 -05:00
bbedward
db1de9df38 keybinds: fix empty string args, more writable provider options 2026-01-07 15:38:44 -05:00
bbedward
3dd21382ba network: support hidden SSIDs 2026-01-07 14:13:03 -05:00
bbedward
ec2b3d0d4b vpn: aggregate all import errors
- we are dumb about importing by just trying to import everythting
- that caused errors to not be represented correctly
- just aggregate them all and present them in toast details
- Better would be to detect the type of file being imported, but this is
  better than nothing
2026-01-07 13:22:56 -05:00
bbedward
a205df1bd6 keybinds: initial support for writable hyprland and mangoWC
fixes #1204
2026-01-07 12:15:38 -05:00
bbedward
e822fa73da cursor: make min/max wider 2026-01-07 10:04:47 -05:00
bbedward
634e75b80c plugins: improve version check 2026-01-07 09:46:55 -05:00
bbedward
ec5b507efc greeter: change hypr startup to exec-once 2026-01-07 09:18:32 -05:00
199 changed files with 12832 additions and 4450 deletions

1
.gitignore vendored
View File

@@ -109,3 +109,4 @@ bin/
.envrc .envrc
.direnv/ .direnv/
quickshell/dms-plugins quickshell/dms-plugins
__pycache__

View File

@@ -514,5 +514,6 @@ func getCommonCommands() []*cobra.Command {
matugenCmd, matugenCmd,
clipboardCmd, clipboardCmd,
doctorCmd, doctorCmd,
configCmd,
} }
} }

View 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)
}

View File

@@ -87,6 +87,8 @@ var (
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`) swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
riverVersionRegex = regexp.MustCompile(`river (\d+\.\d+)`) riverVersionRegex = regexp.MustCompile(`river (\d+\.\d+)`)
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`) wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
) )
var doctorCmd = &cobra.Command{ var doctorCmd = &cobra.Command{
@@ -448,11 +450,13 @@ func checkWindowManagers() []checkResult {
versionRegex *regexp.Regexp versionRegex *regexp.Regexp
commands []string commands []string
}{ }{
{"Hyprland", "hyprctl", "version", hyprlandVersionRegex, []string{"hyprland", "Hyprland"}}, {"Hyprland", "Hyprland", "--version", hyprlandVersionRegex, []string{"hyprland", "Hyprland"}},
{"niri", "niri", "--version", niriVersionRegex, []string{"niri"}}, {"niri", "niri", "--version", niriVersionRegex, []string{"niri"}},
{"Sway", "sway", "--version", swayVersionRegex, []string{"sway"}}, {"Sway", "sway", "--version", swayVersionRegex, []string{"sway"}},
{"River", "river", "-version", riverVersionRegex, []string{"river"}}, {"River", "river", "-version", riverVersionRegex, []string{"river"}},
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}}, {"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
} }
var results []checkResult var results []checkResult
@@ -477,7 +481,7 @@ func checkWindowManagers() []checkResult {
results = append(results, checkResult{ results = append(results, checkResult{
catCompositor, c.name, statusOK, catCompositor, c.name, statusOK,
getVersionFromCommand(c.versionCmd, c.versionArg, c.versionRegex), details, getVersionFromCommand(c.versionCmd, c.versionArg, c.versionRegex), details,
doctorDocsURL + "#compositor", doctorDocsURL + "#compositor-checks",
}) })
} }
@@ -486,7 +490,7 @@ func checkWindowManagers() []checkResult {
catCompositor, "Compositor", statusError, catCompositor, "Compositor", statusError,
"No supported Wayland compositor found", "No supported Wayland compositor found",
"Install Hyprland, niri, Sway, River, or Wayfire", "Install Hyprland, niri, Sway, River, or Wayfire",
doctorDocsURL + "#compositor", doctorDocsURL + "#compositor-checks",
}) })
} }
@@ -498,8 +502,8 @@ func checkWindowManagers() []checkResult {
} }
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string { func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
output, err := exec.Command(cmd, arg).Output() output, err := exec.Command(cmd, arg).CombinedOutput()
if err != nil { if err != nil && len(output) == 0 {
return "installed" return "installed"
} }
@@ -634,19 +638,14 @@ func checkI2CAvailability() checkResult {
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"} return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
} }
func detectNetworkBackend() string { func detectNetworkBackend(stackResult *network.DetectResult) string {
result, err := network.DetectNetworkStack() switch stackResult.Backend {
if err != nil {
return ""
}
switch result.Backend {
case network.BackendNetworkManager: case network.BackendNetworkManager:
return "NetworkManager" return "NetworkManager"
case network.BackendIwd: case network.BackendIwd:
return "iwd" return "iwd"
case network.BackendNetworkd: case network.BackendNetworkd:
if result.HasIwd { if stackResult.HasIwd {
return "iwd + systemd-networkd" return "iwd + systemd-networkd"
} }
return "systemd-networkd" return "systemd-networkd"
@@ -657,75 +656,73 @@ func detectNetworkBackend() string {
} }
} }
func getOptionalDBusStatus(busName string) (status, string) {
if utils.IsDBusServiceAvailable(busName) {
return statusOK, "Available"
} else {
return statusWarn, "Not available"
}
}
func checkOptionalDependencies() []checkResult { func checkOptionalDependencies() []checkResult {
var results []checkResult var results []checkResult
if utils.IsServiceActive("accounts-daemon", false) { optionalFeaturesURL := doctorDocsURL + "#optional-features"
results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusOK, "Running", "User accounts", doctorDocsURL + "#optional-features"})
} else {
results = append(results, checkResult{catOptionalFeatures, "accountsservice", statusWarn, "Not running", "User accounts", doctorDocsURL + "#optional-features"})
}
if utils.IsServiceActive("power-profiles-daemon", false) { accountsStatus, accountsMsg := getOptionalDBusStatus("org.freedesktop.Accounts")
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusOK, "Running", "Power profile management", doctorDocsURL + "#optional-features"}) results = append(results, checkResult{catOptionalFeatures, "accountsservice", accountsStatus, accountsMsg, "User accounts", optionalFeaturesURL})
} else {
results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", statusInfo, "Not running", "Power profile management", doctorDocsURL + "#optional-features"}) ppdStatus, ppdMsg := getOptionalDBusStatus("org.freedesktop.UPower.PowerProfiles")
} results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", ppdStatus, ppdMsg, "Power profile management", optionalFeaturesURL})
logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1")
results = append(results, checkResult{catOptionalFeatures, "logind", logindStatus, logindMsg, "Session management", optionalFeaturesURL})
results = append(results, checkI2CAvailability()) results = append(results, checkI2CAvailability())
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"} terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 { if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", doctorDocsURL + "#optional-features"}) results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
} else { } else {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", doctorDocsURL + "#optional-features"}) results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
} }
networkResult, err := network.DetectNetworkStack()
networkStatus, networkMessage, networkDetails := statusOK, "Not available", "Network management"
if err == nil && networkResult.Backend != network.BackendNone {
networkMessage = detectNetworkBackend(networkResult)
if doctorVerbose {
networkDetails = networkResult.ChosenReason
}
} else {
networkStatus = statusInfo
}
results = append(results, checkResult{catOptionalFeatures, "Network", networkStatus, networkMessage, networkDetails, optionalFeaturesURL})
deps := []struct { deps := []struct {
name, cmd, altCmd, desc string name, cmd, desc string
important bool important bool
}{ }{
{"matugen", "matugen", "", "Dynamic theming", true}, {"matugen", "matugen", "Dynamic theming", true},
{"dgop", "dgop", "", "System monitoring", true}, {"dgop", "dgop", "System monitoring", true},
{"cava", "cava", "", "Audio visualizer", true}, {"cava", "cava", "Audio visualizer", true},
{"khal", "khal", "", "Calendar events", false}, {"khal", "khal", "Calendar events", false},
{"Network", "nmcli", "iwctl", "Network management", false}, {"danksearch", "dsearch", "File search", false},
{"danksearch", "dsearch", "", "File search", false}, {"fprintd", "fprintd-list", "Fingerprint auth", false},
{"loginctl", "loginctl", "", "Session management", false},
{"fprintd", "fprintd-list", "", "Fingerprint auth", false},
} }
for _, d := range deps { for _, d := range deps {
found, foundCmd := utils.CommandExists(d.cmd), d.cmd found := utils.CommandExists(d.cmd)
if !found && d.altCmd != "" && utils.CommandExists(d.altCmd) {
found, foundCmd = true, d.altCmd
}
switch { switch {
case found: case found:
message := "Installed" results = append(results, checkResult{catOptionalFeatures, d.name, statusOK, "Installed", d.desc, optionalFeaturesURL})
details := d.desc
if d.name == "Network" {
result, err := network.DetectNetworkStack()
if err == nil && result.Backend != network.BackendNone {
message = detectNetworkBackend() + " (active)"
if doctorVerbose {
details = result.ChosenReason
}
} else {
switch foundCmd {
case "nmcli":
message = "NetworkManager (installed)"
case "iwctl":
message = "iwd (installed)"
}
}
}
results = append(results, checkResult{catOptionalFeatures, d.name, statusOK, message, details, doctorDocsURL + "#optional-features"})
case d.important: case d.important:
results = append(results, checkResult{catOptionalFeatures, d.name, statusWarn, "Missing", d.desc, doctorDocsURL + "#optional-features"}) results = append(results, checkResult{catOptionalFeatures, d.name, statusWarn, "Missing", d.desc, optionalFeaturesURL})
default: default:
results = append(results, checkResult{catOptionalFeatures, d.name, statusInfo, "Not installed", d.desc, doctorDocsURL + "#optional-features"}) results = append(results, checkResult{catOptionalFeatures, d.name, statusInfo, "Not installed", d.desc, optionalFeaturesURL})
} }
} }
@@ -893,6 +890,10 @@ func printResultLine(r checkResult, styles tui.Styles) {
if doctorVerbose && r.details != "" { if doctorVerbose && r.details != "" {
fmt.Printf(" %s\n", styles.Subtle.Render("└─ "+r.details)) fmt.Printf(" %s\n", styles.Subtle.Render("└─ "+r.details))
} }
if (r.status == statusError || r.status == statusWarn) && r.url != "" {
fmt.Printf(" %s\n", styles.Subtle.Render("→ "+r.url))
}
} }
func printSummary(results []checkResult, qsMissingFeatures bool) { func printSummary(results []checkResult, qsMissingFeatures bool) {

View File

@@ -64,6 +64,7 @@ func init() {
keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds") keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds")
keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat") keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat")
keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)") keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)")
keybindsSetCmd.Flags().String("flags", "", "Hyprland bind flags (e.g., 'e' for repeat, 'l' for locked, 'r' for release)")
keybindsCmd.AddCommand(keybindsListCmd) keybindsCmd.AddCommand(keybindsListCmd)
keybindsCmd.AddCommand(keybindsShowCmd) keybindsCmd.AddCommand(keybindsShowCmd)
@@ -211,6 +212,9 @@ func runKeybindsSet(cmd *cobra.Command, args []string) {
if v, _ := cmd.Flags().GetBool("no-repeat"); v { if v, _ := cmd.Flags().GetBool("no-repeat"); v {
options["repeat"] = false options["repeat"] = false
} }
if v, _ := cmd.Flags().GetString("flags"); v != "" {
options["flags"] = v
}
desc, _ := cmd.Flags().GetString("desc") desc, _ := cmd.Flags().GetString("desc")
if err := writable.SetBind(key, action, desc, options); err != nil { if err := writable.SetBind(key, action, desc, options); err != nil {

View File

@@ -543,7 +543,7 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
return result, result.Error return result, result.Error
} }
if err := cd.deployHyprlandDmsConfigs(dmsDir); err != nil { if err := cd.deployHyprlandDmsConfigs(dmsDir, terminalCommand); err != nil {
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err) result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
return result, result.Error return result, result.Error
} }
@@ -553,13 +553,14 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
return result, nil return result, nil
} }
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string) error { func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalCommand string) error {
configs := []struct { configs := []struct {
name string name string
content string content string
}{ }{
{"colors.conf", HyprColorsConfig}, {"colors.conf", HyprColorsConfig},
{"layout.conf", HyprLayoutConfig}, {"layout.conf", HyprLayoutConfig},
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
{"outputs.conf", ""}, {"outputs.conf", ""},
{"cursor.conf", ""}, {"cursor.conf", ""},
} }

View File

@@ -408,7 +408,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
content, err := os.ReadFile(result.Path) content, err := os.ReadFile(result.Path)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, string(content), "# MONITOR CONFIG") assert.Contains(t, string(content), "# MONITOR CONFIG")
assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty") assert.Contains(t, string(content), "source = ./dms/binds.conf")
assert.Contains(t, string(content), "exec-once = ") assert.Contains(t, string(content), "exec-once = ")
}) })
@@ -444,7 +444,7 @@ general {
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144") assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60") assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
assert.Contains(t, string(newContent), "bind = $mod, T, exec, kitty") assert.Contains(t, string(newContent), "source = ./dms/binds.conf")
assert.NotContains(t, string(newContent), "monitor = eDP-2") assert.NotContains(t, string(newContent), "monitor = eDP-2")
}) })
} }
@@ -461,9 +461,7 @@ func TestHyprlandConfigStructure(t *testing.T) {
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG") assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
assert.Contains(t, HyprlandConfig, "# STARTUP APPS") assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG") assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS") assert.Contains(t, HyprlandConfig, "source = ./dms/binds.conf")
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, {{TERMINAL_COMMAND}}")
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
} }
func TestGhosttyConfigStructure(t *testing.T) { func TestGhosttyConfigStructure(t *testing.T) {

View File

@@ -0,0 +1,156 @@
# === Application Launchers ===
bind = SUPER, T, exec, {{TERMINAL_COMMAND}}
bind = SUPER, space, exec, dms ipc call spotlight toggle
bind = SUPER, V, exec, dms ipc call clipboard toggle
bind = SUPER, M, exec, dms ipc call processlist focusOrToggle
bind = SUPER, comma, exec, dms ipc call settings focusOrToggle
bind = SUPER, N, exec, dms ipc call notifications toggle
bind = SUPER SHIFT, N, exec, dms ipc call notepad toggle
bind = SUPER, Y, exec, dms ipc call dankdash wallpaper
bind = SUPER, TAB, exec, dms ipc call hypr toggleOverview
bind = SUPER, X, exec, dms ipc call powermenu toggle
# === Cheat sheet
bind = SUPER SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
# === Security ===
bind = SUPER ALT, L, exec, dms ipc call lock lock
bind = SUPER SHIFT, E, exit
bind = CTRL ALT, Delete, exec, dms ipc call processlist focusOrToggle
# === Audio Controls ===
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
bindl = , XF86AudioMute, exec, dms ipc call audio mute
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
bindl = , XF86AudioPause, exec, dms ipc call mpris playPause
bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause
bindl = , XF86AudioPrev, exec, dms ipc call mpris previous
bindl = , XF86AudioNext, exec, dms ipc call mpris next
# === 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

View File

@@ -106,173 +106,13 @@ windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture
windowrule = float on, match:class ^(zoom)$ windowrule = float on, match:class ^(zoom)$
# DMS windows floating by default # DMS windows floating by default
windowrule = float on, match:class ^(org.quickshell)$ # ! Hyprland doesn't size these windows correctly so disabling by default here
windowrule = opacity 0.9 0.9, match:float false, match:focus false # windowrule = float on, match:class ^(org.quickshell)$
layerrule = no_anim on, match:namespace ^(quickshell)$ layerrule = no_anim on, match:namespace ^(quickshell)$
# ==================
# KEYBINDINGS
# ==================
$mod = SUPER
# === Application Launchers ===
bind = $mod, T, exec, {{TERMINAL_COMMAND}}
bind = $mod, space, exec, dms ipc call spotlight toggle
bind = $mod, V, exec, dms ipc call clipboard toggle
bind = $mod, M, exec, dms ipc call processlist focusOrToggle
bind = $mod, comma, exec, dms ipc call settings focusOrToggle
bind = $mod, N, exec, dms ipc call notifications toggle
bind = $mod SHIFT, N, exec, dms ipc call notepad toggle
bind = $mod, Y, exec, dms ipc call dankdash wallpaper
bind = $mod, TAB, exec, dms ipc call hypr toggleOverview
# === Cheat sheet
bind = $mod SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
# === Security ===
bind = $mod ALT, L, exec, dms ipc call lock lock
bind = $mod SHIFT, E, exit
bind = CTRL ALT, Delete, exec, dms ipc call processlist focusOrToggle
# === Audio Controls ===
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
bindl = , XF86AudioMute, exec, dms ipc call audio mute
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
bindl = , XF86AudioPause, exec, dms ipc call mpris playPause
bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause
bindl = , XF86AudioPrev, exec, dms ipc call mpris previous
bindl = , XF86AudioNext, exec, dms ipc call mpris next
# === Brightness Controls ===
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
# === Window Management ===
bind = $mod, Q, killactive
bind = $mod, F, fullscreen, 1
bind = $mod SHIFT, F, fullscreen, 0
bind = $mod SHIFT, T, togglefloating
bind = $mod, W, togglegroup
# === Focus Navigation ===
bind = $mod, left, movefocus, l
bind = $mod, down, movefocus, d
bind = $mod, up, movefocus, u
bind = $mod, right, movefocus, r
bind = $mod, H, movefocus, l
bind = $mod, J, movefocus, d
bind = $mod, K, movefocus, u
bind = $mod, L, movefocus, r
# === Window Movement ===
bind = $mod SHIFT, left, movewindow, l
bind = $mod SHIFT, down, movewindow, d
bind = $mod SHIFT, up, movewindow, u
bind = $mod SHIFT, right, movewindow, r
bind = $mod SHIFT, H, movewindow, l
bind = $mod SHIFT, J, movewindow, d
bind = $mod SHIFT, K, movewindow, u
bind = $mod SHIFT, L, movewindow, r
# === Column Navigation ===
bind = $mod, Home, focuswindow, first
bind = $mod, End, focuswindow, last
# === Monitor Navigation ===
bind = $mod CTRL, left, focusmonitor, l
bind = $mod CTRL, right, focusmonitor, r
bind = $mod CTRL, H, focusmonitor, l
bind = $mod CTRL, J, focusmonitor, d
bind = $mod CTRL, K, focusmonitor, u
bind = $mod CTRL, L, focusmonitor, r
# === Move to Monitor ===
bind = $mod SHIFT CTRL, left, movewindow, mon:l
bind = $mod SHIFT CTRL, down, movewindow, mon:d
bind = $mod SHIFT CTRL, up, movewindow, mon:u
bind = $mod SHIFT CTRL, right, movewindow, mon:r
bind = $mod SHIFT CTRL, H, movewindow, mon:l
bind = $mod SHIFT CTRL, J, movewindow, mon:d
bind = $mod SHIFT CTRL, K, movewindow, mon:u
bind = $mod SHIFT CTRL, L, movewindow, mon:r
# === Workspace Navigation ===
bind = $mod, Page_Down, workspace, e+1
bind = $mod, Page_Up, workspace, e-1
bind = $mod, U, workspace, e+1
bind = $mod, I, workspace, e-1
bind = $mod CTRL, down, movetoworkspace, e+1
bind = $mod CTRL, up, movetoworkspace, e-1
bind = $mod CTRL, U, movetoworkspace, e+1
bind = $mod CTRL, I, movetoworkspace, e-1
# === Move Workspaces ===
bind = $mod SHIFT, Page_Down, movetoworkspace, e+1
bind = $mod SHIFT, Page_Up, movetoworkspace, e-1
bind = $mod SHIFT, U, movetoworkspace, e+1
bind = $mod SHIFT, I, movetoworkspace, e-1
# === Mouse Wheel Navigation ===
bind = $mod, mouse_down, workspace, e+1
bind = $mod, mouse_up, workspace, e-1
bind = $mod CTRL, mouse_down, movetoworkspace, e+1
bind = $mod CTRL, mouse_up, movetoworkspace, e-1
# === Numbered Workspaces ===
bind = $mod, 1, workspace, 1
bind = $mod, 2, workspace, 2
bind = $mod, 3, workspace, 3
bind = $mod, 4, workspace, 4
bind = $mod, 5, workspace, 5
bind = $mod, 6, workspace, 6
bind = $mod, 7, workspace, 7
bind = $mod, 8, workspace, 8
bind = $mod, 9, workspace, 9
# === Move to Numbered Workspaces ===
bind = $mod SHIFT, 1, movetoworkspace, 1
bind = $mod SHIFT, 2, movetoworkspace, 2
bind = $mod SHIFT, 3, movetoworkspace, 3
bind = $mod SHIFT, 4, movetoworkspace, 4
bind = $mod SHIFT, 5, movetoworkspace, 5
bind = $mod SHIFT, 6, movetoworkspace, 6
bind = $mod SHIFT, 7, movetoworkspace, 7
bind = $mod SHIFT, 8, movetoworkspace, 8
bind = $mod SHIFT, 9, movetoworkspace, 9
# === Column Management ===
bind = $mod, bracketleft, layoutmsg, preselect l
bind = $mod, bracketright, layoutmsg, preselect r
# === Sizing & Layout ===
bind = $mod, R, layoutmsg, togglesplit
bind = $mod CTRL, F, resizeactive, exact 100%
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
bindmd = $mod, mouse:272, Move window, movewindow
bindmd = $mod, mouse:273, Resize window, resizewindow
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
bindd = $mod, code:20, Expand window left, resizeactive, -100 0
bindd = $mod, code:21, Shrink window left, resizeactive, 100 0
# === Manual Sizing ===
binde = $mod, minus, resizeactive, -10% 0
binde = $mod, equal, resizeactive, 10% 0
binde = $mod SHIFT, minus, resizeactive, 0 -10%
binde = $mod SHIFT, equal, resizeactive, 0 10%
# === Screenshots ===
bind = , Print, exec, dms screenshot
bind = CTRL, Print, exec, dms screenshot full
bind = ALT, Print, exec, dms screenshot window
# === System Controls ===
bind = $mod SHIFT, P, dpms, toggle
source = ./dms/colors.conf source = ./dms/colors.conf
source = ./dms/outputs.conf source = ./dms/outputs.conf
source = ./dms/layout.conf source = ./dms/layout.conf
source = ./dms/cursor.conf source = ./dms/cursor.conf
source = ./dms/binds.conf

View File

@@ -15,6 +15,8 @@ binds {
Mod+M hotkey-overlay-title="Task Manager" { Mod+M hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle"; spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
} }
Super+X hotkey-overlay-title="Power Menu: Toggle" { spawn "dms" "ipc" "call" "powermenu" "toggle"; }
Mod+Comma hotkey-overlay-title="Settings" { Mod+Comma hotkey-overlay-title="Settings" {
spawn "dms" "ipc" "call" "settings" "focusOrToggle"; spawn "dms" "ipc" "call" "settings" "focusOrToggle";
} }

View File

@@ -10,3 +10,6 @@ var HyprColorsConfig string
//go:embed embedded/hypr-layout.conf //go:embed embedded/hypr-layout.conf
var HyprLayoutConfig string var HyprLayoutConfig string
//go:embed embedded/hypr-binds.conf
var HyprBindsConfig string

View File

@@ -153,7 +153,7 @@ func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageM
} }
func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping { func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"} return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "sdegler/hyprland"}
} }
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping { func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {

View File

@@ -108,7 +108,6 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
packages := map[string]PackageMapping{ packages := map[string]PackageMapping{
// Standard zypper packages // Standard zypper packages
"git": {Name: "git", Repository: RepoTypeSystem}, "git": {Name: "git", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem}, "kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem}, "alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, "xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
@@ -117,6 +116,7 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
// DMS packages from OBS // DMS packages from OBS
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": o.getQuickshellMapping(variants["quickshell"]), "quickshell": o.getQuickshellMapping(variants["quickshell"]),
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
} }

View File

@@ -2,45 +2,93 @@ package providers
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"sort"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
type HyprlandProvider struct { type HyprlandProvider struct {
configPath string configPath string
dmsBindsIncluded bool
parsed bool
} }
func NewHyprlandProvider(configPath string) *HyprlandProvider { func NewHyprlandProvider(configPath string) *HyprlandProvider {
if configPath == "" { if configPath == "" {
configPath = "$HOME/.config/hypr" configPath = defaultHyprlandConfigDir()
} }
return &HyprlandProvider{ return &HyprlandProvider{
configPath: configPath, configPath: configPath,
} }
} }
func defaultHyprlandConfigDir() string {
configDir, err := os.UserConfigDir()
if err != nil {
return ""
}
return filepath.Join(configDir, "hypr")
}
func (h *HyprlandProvider) Name() string { func (h *HyprlandProvider) Name() string {
return "hyprland" return "hyprland"
} }
func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
section, err := ParseHyprlandKeys(h.configPath) result, err := ParseHyprlandKeysWithDMS(h.configPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse hyprland config: %w", err) return nil, fmt.Errorf("failed to parse hyprland config: %w", err)
} }
categorizedBinds := make(map[string][]keybinds.Keybind) h.dmsBindsIncluded = result.DMSBindsIncluded
h.convertSection(section, "", categorizedBinds) h.parsed = true
return &keybinds.CheatSheet{ categorizedBinds := make(map[string][]keybinds.Keybind)
Title: "Hyprland Keybinds", h.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs)
Provider: h.Name(),
Binds: categorizedBinds, sheet := &keybinds.CheatSheet{
}, nil Title: "Hyprland Keybinds",
Provider: h.Name(),
Binds: categorizedBinds,
DMSBindsIncluded: result.DMSBindsIncluded,
}
if result.DMSStatus != nil {
sheet.DMSStatus = &keybinds.DMSBindsStatus{
Exists: result.DMSStatus.Exists,
Included: result.DMSStatus.Included,
IncludePosition: result.DMSStatus.IncludePosition,
TotalIncludes: result.DMSStatus.TotalIncludes,
BindsAfterDMS: result.DMSStatus.BindsAfterDMS,
Effective: result.DMSStatus.Effective,
OverriddenBy: result.DMSStatus.OverriddenBy,
StatusMessage: result.DMSStatus.StatusMessage,
}
}
return sheet, nil
} }
func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) { func (h *HyprlandProvider) HasDMSBindsIncluded() bool {
if h.parsed {
return h.dmsBindsIncluded
}
result, err := ParseHyprlandKeysWithDMS(h.configPath)
if err != nil {
return false
}
h.dmsBindsIncluded = result.DMSBindsIncluded
h.parsed = true
return h.dmsBindsIncluded
}
func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*HyprlandKeyBinding) {
currentSubcat := subcategory currentSubcat := subcategory
if section.Name != "" { if section.Name != "" {
currentSubcat = section.Name currentSubcat = section.Name
@@ -48,12 +96,12 @@ func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory
for _, kb := range section.Keybinds { for _, kb := range section.Keybinds {
category := h.categorizeByDispatcher(kb.Dispatcher) category := h.categorizeByDispatcher(kb.Dispatcher)
bind := h.convertKeybind(&kb, currentSubcat) bind := h.convertKeybind(&kb, currentSubcat, conflicts)
categorizedBinds[category] = append(categorizedBinds[category], bind) categorizedBinds[category] = append(categorizedBinds[category], bind)
} }
for _, child := range section.Children { for _, child := range section.Children {
h.convertSection(&child, currentSubcat, categorizedBinds) h.convertSection(&child, currentSubcat, categorizedBinds, conflicts)
} }
} }
@@ -85,8 +133,8 @@ func (h *HyprlandProvider) categorizeByDispatcher(dispatcher string) string {
} }
} }
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string) keybinds.Keybind { func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string, conflicts map[string]*HyprlandKeyBinding) keybinds.Keybind {
key := h.formatKey(kb) keyStr := h.formatKey(kb)
rawAction := h.formatRawAction(kb.Dispatcher, kb.Params) rawAction := h.formatRawAction(kb.Dispatcher, kb.Params)
desc := kb.Comment desc := kb.Comment
@@ -94,12 +142,33 @@ func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory st
desc = rawAction desc = rawAction
} }
return keybinds.Keybind{ source := "config"
Key: key, if strings.Contains(kb.Source, "dms/binds.conf") {
source = "dms"
}
bind := keybinds.Keybind{
Key: keyStr,
Description: desc, Description: desc,
Action: rawAction, Action: rawAction,
Subcategory: subcategory, Subcategory: subcategory,
Source: source,
Flags: kb.Flags,
} }
if source == "dms" && conflicts != nil {
normalizedKey := strings.ToLower(keyStr)
if conflictKb, ok := conflicts[normalizedKey]; ok {
bind.Conflict = &keybinds.Keybind{
Key: keyStr,
Description: conflictKb.Comment,
Action: h.formatRawAction(conflictKb.Dispatcher, conflictKb.Params),
Source: "config",
}
}
}
return bind
} }
func (h *HyprlandProvider) formatRawAction(dispatcher, params string) string { func (h *HyprlandProvider) formatRawAction(dispatcher, params string) string {
@@ -115,3 +184,314 @@ func (h *HyprlandProvider) formatKey(kb *HyprlandKeyBinding) string {
parts = append(parts, kb.Key) parts = append(parts, kb.Key)
return strings.Join(parts, "+") return strings.Join(parts, "+")
} }
func (h *HyprlandProvider) GetOverridePath() string {
expanded, err := utils.ExpandPath(h.configPath)
if err != nil {
return filepath.Join(h.configPath, "dms", "binds.conf")
}
return filepath.Join(expanded, "dms", "binds.conf")
}
func (h *HyprlandProvider) validateAction(action string) error {
action = strings.TrimSpace(action)
switch {
case action == "":
return fmt.Errorf("action cannot be empty")
case action == "exec" || action == "exec ":
return fmt.Errorf("exec dispatcher requires arguments")
case strings.HasPrefix(action, "exec "):
rest := strings.TrimSpace(strings.TrimPrefix(action, "exec "))
if rest == "" {
return fmt.Errorf("exec dispatcher requires arguments")
}
}
return nil
}
func (h *HyprlandProvider) SetBind(key, action, description string, options map[string]any) error {
if err := h.validateAction(action); err != nil {
return err
}
overridePath := h.GetOverridePath()
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
return fmt.Errorf("failed to create dms directory: %w", err)
}
existingBinds, err := h.loadOverrideBinds()
if err != nil {
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,
}
return h.writeOverrideBinds(existingBinds)
}
func (h *HyprlandProvider) RemoveBind(key string) error {
existingBinds, err := h.loadOverrideBinds()
if err != nil {
return nil
}
normalizedKey := strings.ToLower(key)
delete(existingBinds, normalizedKey)
return h.writeOverrideBinds(existingBinds)
}
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
}
func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind, error) {
overridePath := h.GetOverridePath()
binds := make(map[string]*hyprlandOverrideBind)
data, err := os.ReadFile(overridePath)
if os.IsNotExist(err) {
return binds, nil
}
if err != nil {
return nil, err
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if !strings.HasPrefix(line, "bind") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) < 2 {
continue
}
// Extract flags from bind type
bindType := strings.TrimSpace(parts[0])
flags := extractBindFlags(bindType)
hasDescFlag := strings.Contains(flags, "d")
content := strings.TrimSpace(parts[1])
commentParts := strings.SplitN(content, "#", 2)
bindContent := strings.TrimSpace(commentParts[0])
var comment string
if len(commentParts) > 1 {
comment = strings.TrimSpace(commentParts[1])
}
// For bindd, format is: mods, key, description, dispatcher, params
var minFields, descIndex, dispatcherIndex int
if hasDescFlag {
minFields = 4
descIndex = 2
dispatcherIndex = 3
} else {
minFields = 3
dispatcherIndex = 2
}
fields := strings.SplitN(bindContent, ",", minFields+2)
if len(fields) < minFields {
continue
}
mods := strings.TrimSpace(fields[0])
keyName := strings.TrimSpace(fields[1])
var dispatcher, params string
if hasDescFlag {
if comment == "" {
comment = strings.TrimSpace(fields[descIndex])
}
dispatcher = strings.TrimSpace(fields[dispatcherIndex])
if len(fields) > dispatcherIndex+1 {
paramParts := fields[dispatcherIndex+1:]
params = strings.TrimSpace(strings.Join(paramParts, ","))
}
} else {
dispatcher = strings.TrimSpace(fields[dispatcherIndex])
if len(fields) > dispatcherIndex+1 {
paramParts := fields[dispatcherIndex+1:]
params = strings.TrimSpace(strings.Join(paramParts, ","))
}
}
keyStr := h.buildKeyString(mods, keyName)
normalizedKey := strings.ToLower(keyStr)
action := dispatcher
if params != "" {
action = dispatcher + " " + params
}
binds[normalizedKey] = &hyprlandOverrideBind{
Key: keyStr,
Action: action,
Description: comment,
Flags: flags,
}
}
return binds, nil
}
func (h *HyprlandProvider) buildKeyString(mods, key string) string {
if mods == "" {
return key
}
modList := strings.FieldsFunc(mods, func(r rune) bool {
return r == '+' || r == ' '
})
parts := append(modList, key)
return strings.Join(parts, "+")
}
func (h *HyprlandProvider) getBindSortPriority(action string) int {
switch {
case strings.HasPrefix(action, "exec") && strings.Contains(action, "dms"):
return 0
case strings.Contains(action, "workspace"):
return 1
case strings.Contains(action, "window") || strings.Contains(action, "focus") ||
strings.Contains(action, "move") || strings.Contains(action, "swap") ||
strings.Contains(action, "resize"):
return 2
case strings.Contains(action, "monitor"):
return 3
case strings.HasPrefix(action, "exec"):
return 4
case action == "exit" || strings.Contains(action, "dpms"):
return 5
default:
return 6
}
}
func (h *HyprlandProvider) writeOverrideBinds(binds map[string]*hyprlandOverrideBind) error {
overridePath := h.GetOverridePath()
content := h.generateBindsContent(binds)
return os.WriteFile(overridePath, []byte(content), 0644)
}
func (h *HyprlandProvider) generateBindsContent(binds map[string]*hyprlandOverrideBind) string {
if len(binds) == 0 {
return ""
}
bindList := make([]*hyprlandOverrideBind, 0, len(binds))
for _, bind := range binds {
bindList = append(bindList, bind)
}
sort.Slice(bindList, func(i, j int) bool {
pi, pj := h.getBindSortPriority(bindList[i].Action), h.getBindSortPriority(bindList[j].Action)
if pi != pj {
return pi < pj
}
return bindList[i].Key < bindList[j].Key
})
var sb strings.Builder
for _, bind := range bindList {
h.writeBindLine(&sb, bind)
}
return sb.String()
}
func (h *HyprlandProvider) writeBindLine(sb *strings.Builder, bind *hyprlandOverrideBind) {
mods, key := h.parseKeyString(bind.Key)
dispatcher, params := h.parseAction(bind.Action)
// Write bind type with flags (e.g., "bind", "binde", "bindel")
sb.WriteString("bind")
if bind.Flags != "" {
sb.WriteString(bind.Flags)
}
sb.WriteString(" = ")
sb.WriteString(mods)
sb.WriteString(", ")
sb.WriteString(key)
sb.WriteString(", ")
// For bindd (description flag), include description before dispatcher
if strings.Contains(bind.Flags, "d") && bind.Description != "" {
sb.WriteString(bind.Description)
sb.WriteString(", ")
}
sb.WriteString(dispatcher)
if params != "" {
sb.WriteString(", ")
sb.WriteString(params)
}
// Only add comment if not using bindd (which has inline description)
if bind.Description != "" && !strings.Contains(bind.Flags, "d") {
sb.WriteString(" # ")
sb.WriteString(bind.Description)
}
sb.WriteString("\n")
}
func (h *HyprlandProvider) parseKeyString(keyStr string) (mods, key string) {
parts := strings.Split(keyStr, "+")
switch len(parts) {
case 0:
return "", keyStr
case 1:
return "", parts[0]
default:
return strings.Join(parts[:len(parts)-1], " "), parts[len(parts)-1]
}
}
func (h *HyprlandProvider) parseAction(action string) (dispatcher, params string) {
parts := strings.SplitN(action, " ", 2)
switch len(parts) {
case 0:
return action, ""
case 1:
dispatcher = parts[0]
default:
dispatcher = parts[0]
params = parts[1]
}
// Convert internal spawn format to Hyprland's exec
if dispatcher == "spawn" {
dispatcher = "exec"
}
return dispatcher, params
}

View File

@@ -23,6 +23,8 @@ type HyprlandKeyBinding struct {
Dispatcher string `json:"dispatcher"` Dispatcher string `json:"dispatcher"`
Params string `json:"params"` Params string `json:"params"`
Comment string `json:"comment"` 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 { type HyprlandSection struct {
@@ -32,14 +34,36 @@ type HyprlandSection struct {
} }
type HyprlandParser struct { type HyprlandParser struct {
contentLines []string contentLines []string
readingLine int readingLine int
configDir string
currentSource string
dmsBindsExists bool
dmsBindsIncluded bool
includeCount int
dmsIncludePos int
bindsAfterDMS int
dmsBindKeys map[string]bool
configBindKeys map[string]bool
conflictingConfigs map[string]*HyprlandKeyBinding
bindMap map[string]*HyprlandKeyBinding
bindOrder []string
processedFiles map[string]bool
dmsProcessed bool
} }
func NewHyprlandParser() *HyprlandParser { func NewHyprlandParser(configDir string) *HyprlandParser {
return &HyprlandParser{ return &HyprlandParser{
contentLines: []string{}, contentLines: []string{},
readingLine: 0, readingLine: 0,
configDir: configDir,
dmsIncludePos: -1,
dmsBindKeys: make(map[string]bool),
configBindKeys: make(map[string]bool),
conflictingConfigs: make(map[string]*HyprlandKeyBinding),
bindMap: make(map[string]*HyprlandKeyBinding),
bindOrder: []string{},
processedFiles: make(map[string]bool),
} }
} }
@@ -195,71 +219,7 @@ func hyprlandAutogenerateComment(dispatcher, params string) string {
func (p *HyprlandParser) getKeybindAtLine(lineNumber int) *HyprlandKeyBinding { func (p *HyprlandParser) getKeybindAtLine(lineNumber int) *HyprlandKeyBinding {
line := p.contentLines[lineNumber] line := p.contentLines[lineNumber]
parts := strings.SplitN(line, "=", 2) return p.parseBindLine(line)
if len(parts) < 2 {
return nil
}
keys := parts[1]
keyParts := strings.SplitN(keys, "#", 2)
keys = keyParts[0]
var comment string
if len(keyParts) > 1 {
comment = strings.TrimSpace(keyParts[1])
}
keyFields := strings.SplitN(keys, ",", 5)
if len(keyFields) < 3 {
return nil
}
mods := strings.TrimSpace(keyFields[0])
key := strings.TrimSpace(keyFields[1])
dispatcher := strings.TrimSpace(keyFields[2])
var params string
if len(keyFields) > 3 {
paramParts := keyFields[3:]
params = strings.TrimSpace(strings.Join(paramParts, ","))
}
if comment != "" {
if strings.HasPrefix(comment, HideComment) {
return nil
}
} else {
comment = hyprlandAutogenerateComment(dispatcher, params)
}
var modList []string
if mods != "" {
modstring := mods + string(ModSeparators[0])
p := 0
for index, char := range modstring {
isModSep := false
for _, sep := range ModSeparators {
if char == sep {
isModSep = true
break
}
}
if isModSep {
if index-p > 1 {
modList = append(modList, modstring[p:index])
}
p = index + 1
}
}
}
return &HyprlandKeyBinding{
Mods: modList,
Key: key,
Dispatcher: dispatcher,
Params: params,
Comment: comment,
}
} }
func (p *HyprlandParser) getBindsRecursive(currentContent *HyprlandSection, scope int) *HyprlandSection { func (p *HyprlandParser) getBindsRecursive(currentContent *HyprlandSection, scope int) *HyprlandSection {
@@ -320,9 +280,348 @@ func (p *HyprlandParser) ParseKeys() *HyprlandSection {
} }
func ParseHyprlandKeys(path string) (*HyprlandSection, error) { func ParseHyprlandKeys(path string) (*HyprlandSection, error) {
parser := NewHyprlandParser() parser := NewHyprlandParser(path)
if err := parser.ReadContent(path); err != nil { if err := parser.ReadContent(path); err != nil {
return nil, err return nil, err
} }
return parser.ParseKeys(), nil return parser.ParseKeys(), nil
} }
type HyprlandParseResult struct {
Section *HyprlandSection
DMSBindsIncluded bool
DMSStatus *HyprlandDMSStatus
ConflictingConfigs map[string]*HyprlandKeyBinding
}
type HyprlandDMSStatus struct {
Exists bool
Included bool
IncludePosition int
TotalIncludes int
BindsAfterDMS int
Effective bool
OverriddenBy int
StatusMessage string
}
func (p *HyprlandParser) buildDMSStatus() *HyprlandDMSStatus {
status := &HyprlandDMSStatus{
Exists: p.dmsBindsExists,
Included: p.dmsBindsIncluded,
IncludePosition: p.dmsIncludePos,
TotalIncludes: p.includeCount,
BindsAfterDMS: p.bindsAfterDMS,
}
switch {
case !p.dmsBindsExists:
status.Effective = false
status.StatusMessage = "dms/binds.conf does not exist"
case !p.dmsBindsIncluded:
status.Effective = false
status.StatusMessage = "dms/binds.conf is not sourced in config"
case p.bindsAfterDMS > 0:
status.Effective = true
status.OverriddenBy = p.bindsAfterDMS
status.StatusMessage = "Some DMS binds may be overridden by config binds"
default:
status.Effective = true
status.StatusMessage = "DMS binds are active"
}
return status
}
func (p *HyprlandParser) formatBindKey(kb *HyprlandKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}
func (p *HyprlandParser) normalizeKey(key string) string {
return strings.ToLower(key)
}
func (p *HyprlandParser) addBind(kb *HyprlandKeyBinding) bool {
key := p.formatBindKey(kb)
normalizedKey := p.normalizeKey(key)
isDMSBind := strings.Contains(kb.Source, "dms/binds.conf")
if isDMSBind {
p.dmsBindKeys[normalizedKey] = true
} else if p.dmsBindKeys[normalizedKey] {
p.bindsAfterDMS++
p.conflictingConfigs[normalizedKey] = kb
p.configBindKeys[normalizedKey] = true
return false
} else {
p.configBindKeys[normalizedKey] = true
}
if _, exists := p.bindMap[normalizedKey]; !exists {
p.bindOrder = append(p.bindOrder, key)
}
p.bindMap[normalizedKey] = kb
return true
}
func (p *HyprlandParser) ParseWithDMS() (*HyprlandSection, error) {
expandedDir, err := utils.ExpandPath(p.configDir)
if err != nil {
return nil, err
}
dmsBindsPath := filepath.Join(expandedDir, "dms", "binds.conf")
if _, err := os.Stat(dmsBindsPath); err == nil {
p.dmsBindsExists = true
}
mainConfig := filepath.Join(expandedDir, "hyprland.conf")
section, err := p.parseFileWithSource(mainConfig, "")
if err != nil {
return nil, err
}
if p.dmsBindsExists && !p.dmsProcessed {
p.parseDMSBindsDirectly(dmsBindsPath, section)
}
return section, nil
}
func (p *HyprlandParser) parseFileWithSource(filePath, sectionName string) (*HyprlandSection, error) {
absPath, err := filepath.Abs(filePath)
if err != nil {
return nil, err
}
if p.processedFiles[absPath] {
return &HyprlandSection{Name: sectionName}, nil
}
p.processedFiles[absPath] = true
data, err := os.ReadFile(absPath)
if err != nil {
return nil, err
}
prevSource := p.currentSource
p.currentSource = absPath
section := &HyprlandSection{Name: sectionName}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "source") {
p.handleSource(trimmed, section, filepath.Dir(absPath))
continue
}
if !strings.HasPrefix(trimmed, "bind") {
continue
}
kb := p.parseBindLine(line)
if kb == nil {
continue
}
kb.Source = p.currentSource
if p.addBind(kb) {
section.Keybinds = append(section.Keybinds, *kb)
}
}
p.currentSource = prevSource
return section, nil
}
func (p *HyprlandParser) handleSource(line string, section *HyprlandSection, baseDir string) {
parts := strings.SplitN(line, "=", 2)
if len(parts) < 2 {
return
}
sourcePath := strings.TrimSpace(parts[1])
isDMSSource := sourcePath == "dms/binds.conf" || strings.HasSuffix(sourcePath, "/dms/binds.conf")
p.includeCount++
if isDMSSource {
p.dmsBindsIncluded = true
p.dmsIncludePos = p.includeCount
p.dmsProcessed = true
}
fullPath := sourcePath
if !filepath.IsAbs(sourcePath) {
fullPath = filepath.Join(baseDir, sourcePath)
}
expanded, err := utils.ExpandPath(fullPath)
if err != nil {
return
}
includedSection, err := p.parseFileWithSource(expanded, "")
if err != nil {
return
}
section.Children = append(section.Children, *includedSection)
}
func (p *HyprlandParser) parseDMSBindsDirectly(dmsBindsPath string, section *HyprlandSection) {
data, err := os.ReadFile(dmsBindsPath)
if err != nil {
return
}
prevSource := p.currentSource
p.currentSource = dmsBindsPath
lines := strings.Split(string(data), "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "bind") {
continue
}
kb := p.parseBindLine(line)
if kb == nil {
continue
}
kb.Source = dmsBindsPath
if p.addBind(kb) {
section.Keybinds = append(section.Keybinds, *kb)
}
}
p.currentSource = prevSource
p.dmsProcessed = true
}
func (p *HyprlandParser) parseBindLine(line string) *HyprlandKeyBinding {
parts := strings.SplitN(line, "=", 2)
if len(parts) < 2 {
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]
var comment string
if len(keyParts) > 1 {
comment = strings.TrimSpace(keyParts[1])
}
// 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])
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) {
return nil
}
if comment == "" {
comment = hyprlandAutogenerateComment(dispatcher, params)
}
var modList []string
if mods != "" {
modstring := mods + string(ModSeparators[0])
idx := 0
for index, char := range modstring {
isModSep := false
for _, sep := range ModSeparators {
if char == sep {
isModSep = true
break
}
}
if isModSep {
if index-idx > 1 {
modList = append(modList, modstring[idx:index])
}
idx = index + 1
}
}
}
return &HyprlandKeyBinding{
Mods: modList,
Key: key,
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()
if err != nil {
return nil, err
}
return &HyprlandParseResult{
Section: section,
DMSBindsIncluded: parser.dmsBindsIncluded,
DMSStatus: parser.buildDMSStatus(),
ConflictingConfigs: parser.conflictingConfigs,
}, nil
}

View File

@@ -130,7 +130,7 @@ func TestHyprlandGetKeybindAtLine(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
parser := NewHyprlandParser() parser := NewHyprlandParser("")
parser.contentLines = []string{tt.line} parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)
@@ -285,7 +285,7 @@ func TestHyprlandReadContentMultipleFiles(t *testing.T) {
t.Fatalf("Failed to write file2: %v", err) t.Fatalf("Failed to write file2: %v", err)
} }
parser := NewHyprlandParser() parser := NewHyprlandParser("")
if err := parser.ReadContent(tmpDir); err != nil { if err := parser.ReadContent(tmpDir); err != nil {
t.Fatalf("ReadContent failed: %v", err) t.Fatalf("ReadContent failed: %v", err)
} }
@@ -343,7 +343,7 @@ func TestHyprlandReadContentWithTildeExpansion(t *testing.T) {
t.Skip("Cannot create relative path") t.Skip("Cannot create relative path")
} }
parser := NewHyprlandParser() parser := NewHyprlandParser("")
tildePathMatch := "~/" + relPath tildePathMatch := "~/" + relPath
err = parser.ReadContent(tildePathMatch) err = parser.ReadContent(tildePathMatch)
@@ -353,7 +353,7 @@ func TestHyprlandReadContentWithTildeExpansion(t *testing.T) {
} }
func TestHyprlandKeybindWithParamsContainingCommas(t *testing.T) { func TestHyprlandKeybindWithParamsContainingCommas(t *testing.T) {
parser := NewHyprlandParser() parser := NewHyprlandParser("")
parser.contentLines = []string{"bind = SUPER, R, exec, notify-send 'Title' 'Message, with comma'"} parser.contentLines = []string{"bind = SUPER, R, exec, notify-send 'Title' 'Message, with comma'"}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)
@@ -394,3 +394,126 @@ bind = SUPER, T, exec, kitty
t.Errorf("Expected 2 keybinds (comments ignored), got %d", len(section.Keybinds)) t.Errorf("Expected 2 keybinds (comments ignored), got %d", len(section.Keybinds))
} }
} }
func TestExtractBindFlags(t *testing.T) {
tests := []struct {
bindType string
expected string
}{
{"bind", ""},
{"binde", "e"},
{"bindl", "l"},
{"bindr", "r"},
{"bindd", "d"},
{"bindo", "o"},
{"bindel", "el"},
{"bindler", "ler"},
{"bindem", "em"},
{" bind ", ""},
{" binde ", "e"},
{"notbind", ""},
{"", ""},
}
for _, tt := range tests {
t.Run(tt.bindType, func(t *testing.T) {
result := extractBindFlags(tt.bindType)
if result != tt.expected {
t.Errorf("extractBindFlags(%q) = %q, want %q", tt.bindType, result, tt.expected)
}
})
}
}
func TestHyprlandBindFlags(t *testing.T) {
tests := []struct {
name string
line string
expectedFlags string
expectedKey string
expectedDisp string
expectedDesc string
}{
{
name: "regular bind",
line: "bind = SUPER, Q, killactive",
expectedFlags: "",
expectedKey: "Q",
expectedDisp: "killactive",
expectedDesc: "Close window",
},
{
name: "binde (repeat on hold)",
line: "binde = , XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+",
expectedFlags: "e",
expectedKey: "XF86AudioRaiseVolume",
expectedDisp: "exec",
expectedDesc: "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+",
},
{
name: "bindl (locked/inhibitor bypass)",
line: "bindl = , XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-",
expectedFlags: "l",
expectedKey: "XF86AudioLowerVolume",
expectedDisp: "exec",
expectedDesc: "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-",
},
{
name: "bindr (release trigger)",
line: "bindr = SUPER, SUPER_L, exec, pkill wofi || wofi",
expectedFlags: "r",
expectedKey: "SUPER_L",
expectedDisp: "exec",
expectedDesc: "pkill wofi || wofi",
},
{
name: "bindd (description)",
line: "bindd = SUPER, Q, Open my favourite terminal, exec, kitty",
expectedFlags: "d",
expectedKey: "Q",
expectedDisp: "exec",
expectedDesc: "Open my favourite terminal",
},
{
name: "bindo (long press)",
line: "bindo = SUPER, XF86AudioNext, exec, playerctl next",
expectedFlags: "o",
expectedKey: "XF86AudioNext",
expectedDisp: "exec",
expectedDesc: "playerctl next",
},
{
name: "bindel (combined flags)",
line: "bindel = , XF86AudioRaiseVolume, exec, wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 5%+",
expectedFlags: "el",
expectedKey: "XF86AudioRaiseVolume",
expectedDisp: "exec",
expectedDesc: "wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 5%+",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := NewHyprlandParser("")
parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0)
if result == nil {
t.Fatal("Expected keybind, got nil")
}
if result.Flags != tt.expectedFlags {
t.Errorf("Flags = %q, want %q", result.Flags, tt.expectedFlags)
}
if result.Key != tt.expectedKey {
t.Errorf("Key = %q, want %q", result.Key, tt.expectedKey)
}
if result.Dispatcher != tt.expectedDisp {
t.Errorf("Dispatcher = %q, want %q", result.Dispatcher, tt.expectedDisp)
}
if result.Comment != tt.expectedDesc {
t.Errorf("Comment = %q, want %q", result.Comment, tt.expectedDesc)
}
})
}
}

View File

@@ -7,35 +7,30 @@ import (
) )
func TestNewHyprlandProvider(t *testing.T) { func TestNewHyprlandProvider(t *testing.T) {
tests := []struct { t.Run("custom path", func(t *testing.T) {
name string p := NewHyprlandProvider("/custom/path")
configPath string if p == nil {
wantPath string t.Fatal("NewHyprlandProvider returned nil")
}{ }
{ if p.configPath != "/custom/path" {
name: "custom path", t.Errorf("configPath = %q, want %q", p.configPath, "/custom/path")
configPath: "/custom/path", }
wantPath: "/custom/path", })
},
{
name: "empty path defaults",
configPath: "",
wantPath: "$HOME/.config/hypr",
},
}
for _, tt := range tests { t.Run("empty path defaults", func(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { p := NewHyprlandProvider("")
p := NewHyprlandProvider(tt.configPath) if p == nil {
if p == nil { t.Fatal("NewHyprlandProvider returned nil")
t.Fatal("NewHyprlandProvider returned nil") }
} configDir, err := os.UserConfigDir()
if err != nil {
if p.configPath != tt.wantPath { t.Fatalf("UserConfigDir failed: %v", err)
t.Errorf("configPath = %q, want %q", p.configPath, tt.wantPath) }
} expected := filepath.Join(configDir, "hypr")
}) if p.configPath != expected {
} t.Errorf("configPath = %q, want %q", p.configPath, expected)
}
})
} }
func TestHyprlandProviderName(t *testing.T) { func TestHyprlandProviderName(t *testing.T) {
@@ -109,7 +104,7 @@ func TestHyprlandProviderGetCheatSheetError(t *testing.T) {
func TestFormatKey(t *testing.T) { func TestFormatKey(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "test.conf") configFile := filepath.Join(tmpDir, "hyprland.conf")
tests := []struct { tests := []struct {
name string name string
@@ -163,7 +158,7 @@ func TestFormatKey(t *testing.T) {
func TestDescriptionFallback(t *testing.T) { func TestDescriptionFallback(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "test.conf") configFile := filepath.Join(tmpDir, "hyprland.conf")
tests := []struct { tests := []struct {
name string name string

View File

@@ -2,46 +2,94 @@ package providers
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"sort"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
type MangoWCProvider struct { type MangoWCProvider struct {
configPath string configPath string
dmsBindsIncluded bool
parsed bool
} }
func NewMangoWCProvider(configPath string) *MangoWCProvider { func NewMangoWCProvider(configPath string) *MangoWCProvider {
if configPath == "" { if configPath == "" {
configPath = "$HOME/.config/mango" configPath = defaultMangoWCConfigDir()
} }
return &MangoWCProvider{ return &MangoWCProvider{
configPath: configPath, configPath: configPath,
} }
} }
func defaultMangoWCConfigDir() string {
configDir, err := os.UserConfigDir()
if err != nil {
return ""
}
return filepath.Join(configDir, "mango")
}
func (m *MangoWCProvider) Name() string { func (m *MangoWCProvider) Name() string {
return "mangowc" return "mangowc"
} }
func (m *MangoWCProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { func (m *MangoWCProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
keybinds_list, err := ParseMangoWCKeys(m.configPath) result, err := ParseMangoWCKeysWithDMS(m.configPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse mangowc config: %w", err) return nil, fmt.Errorf("failed to parse mangowc config: %w", err)
} }
m.dmsBindsIncluded = result.DMSBindsIncluded
m.parsed = true
categorizedBinds := make(map[string][]keybinds.Keybind) categorizedBinds := make(map[string][]keybinds.Keybind)
for _, kb := range keybinds_list { for _, kb := range result.Keybinds {
category := m.categorizeByCommand(kb.Command) category := m.categorizeByCommand(kb.Command)
bind := m.convertKeybind(&kb) bind := m.convertKeybind(&kb, result.ConflictingConfigs)
categorizedBinds[category] = append(categorizedBinds[category], bind) categorizedBinds[category] = append(categorizedBinds[category], bind)
} }
return &keybinds.CheatSheet{ sheet := &keybinds.CheatSheet{
Title: "MangoWC Keybinds", Title: "MangoWC Keybinds",
Provider: m.Name(), Provider: m.Name(),
Binds: categorizedBinds, Binds: categorizedBinds,
}, nil DMSBindsIncluded: result.DMSBindsIncluded,
}
if result.DMSStatus != nil {
sheet.DMSStatus = &keybinds.DMSBindsStatus{
Exists: result.DMSStatus.Exists,
Included: result.DMSStatus.Included,
IncludePosition: result.DMSStatus.IncludePosition,
TotalIncludes: result.DMSStatus.TotalIncludes,
BindsAfterDMS: result.DMSStatus.BindsAfterDMS,
Effective: result.DMSStatus.Effective,
OverriddenBy: result.DMSStatus.OverriddenBy,
StatusMessage: result.DMSStatus.StatusMessage,
}
}
return sheet, nil
}
func (m *MangoWCProvider) HasDMSBindsIncluded() bool {
if m.parsed {
return m.dmsBindsIncluded
}
result, err := ParseMangoWCKeysWithDMS(m.configPath)
if err != nil {
return false
}
m.dmsBindsIncluded = result.DMSBindsIncluded
m.parsed = true
return m.dmsBindsIncluded
} }
func (m *MangoWCProvider) categorizeByCommand(command string) string { func (m *MangoWCProvider) categorizeByCommand(command string) string {
@@ -82,8 +130,8 @@ func (m *MangoWCProvider) categorizeByCommand(command string) string {
} }
} }
func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding) keybinds.Keybind { func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding, conflicts map[string]*MangoWCKeyBinding) keybinds.Keybind {
key := m.formatKey(kb) keyStr := m.formatKey(kb)
rawAction := m.formatRawAction(kb.Command, kb.Params) rawAction := m.formatRawAction(kb.Command, kb.Params)
desc := kb.Comment desc := kb.Comment
@@ -91,11 +139,31 @@ func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding) keybinds.Keybind
desc = rawAction desc = rawAction
} }
return keybinds.Keybind{ source := "config"
Key: key, if strings.Contains(kb.Source, "dms/binds.conf") || strings.Contains(kb.Source, "dms"+string(filepath.Separator)+"binds.conf") {
source = "dms"
}
bind := keybinds.Keybind{
Key: keyStr,
Description: desc, Description: desc,
Action: rawAction, Action: rawAction,
Source: source,
} }
if source == "dms" && conflicts != nil {
normalizedKey := strings.ToLower(keyStr)
if conflictKb, ok := conflicts[normalizedKey]; ok {
bind.Conflict = &keybinds.Keybind{
Key: keyStr,
Description: conflictKb.Comment,
Action: m.formatRawAction(conflictKb.Command, conflictKb.Params),
Source: "config",
}
}
}
return bind
} }
func (m *MangoWCProvider) formatRawAction(command, params string) string { func (m *MangoWCProvider) formatRawAction(command, params string) string {
@@ -111,3 +179,264 @@ func (m *MangoWCProvider) formatKey(kb *MangoWCKeyBinding) string {
parts = append(parts, kb.Key) parts = append(parts, kb.Key)
return strings.Join(parts, "+") return strings.Join(parts, "+")
} }
func (m *MangoWCProvider) GetOverridePath() string {
expanded, err := utils.ExpandPath(m.configPath)
if err != nil {
return filepath.Join(m.configPath, "dms", "binds.conf")
}
return filepath.Join(expanded, "dms", "binds.conf")
}
func (m *MangoWCProvider) validateAction(action string) error {
action = strings.TrimSpace(action)
switch {
case action == "":
return fmt.Errorf("action cannot be empty")
case action == "spawn" || action == "spawn ":
return fmt.Errorf("spawn command requires arguments")
case action == "spawn_shell" || action == "spawn_shell ":
return fmt.Errorf("spawn_shell command requires arguments")
case strings.HasPrefix(action, "spawn "):
rest := strings.TrimSpace(strings.TrimPrefix(action, "spawn "))
if rest == "" {
return fmt.Errorf("spawn command requires arguments")
}
case strings.HasPrefix(action, "spawn_shell "):
rest := strings.TrimSpace(strings.TrimPrefix(action, "spawn_shell "))
if rest == "" {
return fmt.Errorf("spawn_shell command requires arguments")
}
}
return nil
}
func (m *MangoWCProvider) SetBind(key, action, description string, options map[string]any) error {
if err := m.validateAction(action); err != nil {
return err
}
overridePath := m.GetOverridePath()
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
return fmt.Errorf("failed to create dms directory: %w", err)
}
existingBinds, err := m.loadOverrideBinds()
if err != nil {
existingBinds = make(map[string]*mangowcOverrideBind)
}
normalizedKey := strings.ToLower(key)
existingBinds[normalizedKey] = &mangowcOverrideBind{
Key: key,
Action: action,
Description: description,
Options: options,
}
return m.writeOverrideBinds(existingBinds)
}
func (m *MangoWCProvider) RemoveBind(key string) error {
existingBinds, err := m.loadOverrideBinds()
if err != nil {
return nil
}
normalizedKey := strings.ToLower(key)
delete(existingBinds, normalizedKey)
return m.writeOverrideBinds(existingBinds)
}
type mangowcOverrideBind struct {
Key string
Action string
Description string
Options map[string]any
}
func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind, error) {
overridePath := m.GetOverridePath()
binds := make(map[string]*mangowcOverrideBind)
data, err := os.ReadFile(overridePath)
if os.IsNotExist(err) {
return binds, nil
}
if err != nil {
return nil, err
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if !strings.HasPrefix(line, "bind") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) < 2 {
continue
}
content := strings.TrimSpace(parts[1])
commentParts := strings.SplitN(content, "#", 2)
bindContent := strings.TrimSpace(commentParts[0])
var comment string
if len(commentParts) > 1 {
comment = strings.TrimSpace(commentParts[1])
}
fields := strings.SplitN(bindContent, ",", 4)
if len(fields) < 3 {
continue
}
mods := strings.TrimSpace(fields[0])
keyName := strings.TrimSpace(fields[1])
command := strings.TrimSpace(fields[2])
var params string
if len(fields) > 3 {
params = strings.TrimSpace(fields[3])
}
keyStr := m.buildKeyString(mods, keyName)
normalizedKey := strings.ToLower(keyStr)
action := command
if params != "" {
action = command + " " + params
}
binds[normalizedKey] = &mangowcOverrideBind{
Key: keyStr,
Action: action,
Description: comment,
}
}
return binds, nil
}
func (m *MangoWCProvider) buildKeyString(mods, key string) string {
if mods == "" || strings.EqualFold(mods, "none") {
return key
}
modList := strings.FieldsFunc(mods, func(r rune) bool {
return r == '+' || r == ' '
})
parts := append(modList, key)
return strings.Join(parts, "+")
}
func (m *MangoWCProvider) getBindSortPriority(action string) int {
switch {
case strings.HasPrefix(action, "spawn") && strings.Contains(action, "dms"):
return 0
case strings.Contains(action, "view") || strings.Contains(action, "tag"):
return 1
case strings.Contains(action, "focus") || strings.Contains(action, "exchange") ||
strings.Contains(action, "resize") || strings.Contains(action, "move"):
return 2
case strings.Contains(action, "mon"):
return 3
case strings.HasPrefix(action, "spawn"):
return 4
case action == "quit" || action == "reload_config":
return 5
default:
return 6
}
}
func (m *MangoWCProvider) writeOverrideBinds(binds map[string]*mangowcOverrideBind) error {
overridePath := m.GetOverridePath()
content := m.generateBindsContent(binds)
return os.WriteFile(overridePath, []byte(content), 0644)
}
func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverrideBind) string {
if len(binds) == 0 {
return ""
}
bindList := make([]*mangowcOverrideBind, 0, len(binds))
for _, bind := range binds {
bindList = append(bindList, bind)
}
sort.Slice(bindList, func(i, j int) bool {
pi, pj := m.getBindSortPriority(bindList[i].Action), m.getBindSortPriority(bindList[j].Action)
if pi != pj {
return pi < pj
}
return bindList[i].Key < bindList[j].Key
})
var sb strings.Builder
for _, bind := range bindList {
m.writeBindLine(&sb, bind)
}
return sb.String()
}
func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverrideBind) {
mods, key := m.parseKeyString(bind.Key)
command, params := m.parseAction(bind.Action)
sb.WriteString("bind=")
if mods == "" {
sb.WriteString("none")
} else {
sb.WriteString(mods)
}
sb.WriteString(",")
sb.WriteString(key)
sb.WriteString(",")
sb.WriteString(command)
if params != "" {
sb.WriteString(",")
sb.WriteString(params)
}
if bind.Description != "" {
sb.WriteString(" # ")
sb.WriteString(bind.Description)
}
sb.WriteString("\n")
}
func (m *MangoWCProvider) parseKeyString(keyStr string) (mods, key string) {
parts := strings.Split(keyStr, "+")
switch len(parts) {
case 0:
return "", keyStr
case 1:
return "", parts[0]
default:
return strings.Join(parts[:len(parts)-1], "+"), parts[len(parts)-1]
}
}
func (m *MangoWCProvider) parseAction(action string) (command, params string) {
parts := strings.SplitN(action, " ", 2)
switch len(parts) {
case 0:
return action, ""
case 1:
return parts[0], ""
default:
return parts[0], parts[1]
}
}

View File

@@ -21,17 +21,40 @@ type MangoWCKeyBinding struct {
Command string `json:"command"` Command string `json:"command"`
Params string `json:"params"` Params string `json:"params"`
Comment string `json:"comment"` Comment string `json:"comment"`
Source string `json:"source"`
} }
type MangoWCParser struct { type MangoWCParser struct {
contentLines []string contentLines []string
readingLine int readingLine int
configDir string
currentSource string
dmsBindsExists bool
dmsBindsIncluded bool
includeCount int
dmsIncludePos int
bindsAfterDMS int
dmsBindKeys map[string]bool
configBindKeys map[string]bool
conflictingConfigs map[string]*MangoWCKeyBinding
bindMap map[string]*MangoWCKeyBinding
bindOrder []string
processedFiles map[string]bool
dmsProcessed bool
} }
func NewMangoWCParser() *MangoWCParser { func NewMangoWCParser(configDir string) *MangoWCParser {
return &MangoWCParser{ return &MangoWCParser{
contentLines: []string{}, contentLines: []string{},
readingLine: 0, readingLine: 0,
configDir: configDir,
dmsIncludePos: -1,
dmsBindKeys: make(map[string]bool),
configBindKeys: make(map[string]bool),
conflictingConfigs: make(map[string]*MangoWCKeyBinding),
bindMap: make(map[string]*MangoWCKeyBinding),
bindOrder: []string{},
processedFiles: make(map[string]bool),
} }
} }
@@ -294,9 +317,320 @@ func (p *MangoWCParser) ParseKeys() []MangoWCKeyBinding {
} }
func ParseMangoWCKeys(path string) ([]MangoWCKeyBinding, error) { func ParseMangoWCKeys(path string) ([]MangoWCKeyBinding, error) {
parser := NewMangoWCParser() parser := NewMangoWCParser(path)
if err := parser.ReadContent(path); err != nil { if err := parser.ReadContent(path); err != nil {
return nil, err return nil, err
} }
return parser.ParseKeys(), nil return parser.ParseKeys(), nil
} }
type MangoWCParseResult struct {
Keybinds []MangoWCKeyBinding
DMSBindsIncluded bool
DMSStatus *MangoWCDMSStatus
ConflictingConfigs map[string]*MangoWCKeyBinding
}
type MangoWCDMSStatus struct {
Exists bool
Included bool
IncludePosition int
TotalIncludes int
BindsAfterDMS int
Effective bool
OverriddenBy int
StatusMessage string
}
func (p *MangoWCParser) buildDMSStatus() *MangoWCDMSStatus {
status := &MangoWCDMSStatus{
Exists: p.dmsBindsExists,
Included: p.dmsBindsIncluded,
IncludePosition: p.dmsIncludePos,
TotalIncludes: p.includeCount,
BindsAfterDMS: p.bindsAfterDMS,
}
switch {
case !p.dmsBindsExists:
status.Effective = false
status.StatusMessage = "dms/binds.conf does not exist"
case !p.dmsBindsIncluded:
status.Effective = false
status.StatusMessage = "dms/binds.conf is not sourced in config"
case p.bindsAfterDMS > 0:
status.Effective = true
status.OverriddenBy = p.bindsAfterDMS
status.StatusMessage = "Some DMS binds may be overridden by config binds"
default:
status.Effective = true
status.StatusMessage = "DMS binds are active"
}
return status
}
func (p *MangoWCParser) formatBindKey(kb *MangoWCKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}
func (p *MangoWCParser) normalizeKey(key string) string {
return strings.ToLower(key)
}
func (p *MangoWCParser) addBind(kb *MangoWCKeyBinding) {
key := p.formatBindKey(kb)
normalizedKey := p.normalizeKey(key)
isDMSBind := strings.Contains(kb.Source, "dms/binds.conf") || strings.Contains(kb.Source, "dms"+string(os.PathSeparator)+"binds.conf")
if isDMSBind {
p.dmsBindKeys[normalizedKey] = true
} else if p.dmsBindKeys[normalizedKey] {
p.bindsAfterDMS++
p.conflictingConfigs[normalizedKey] = kb
p.configBindKeys[normalizedKey] = true
return
} else {
p.configBindKeys[normalizedKey] = true
}
if _, exists := p.bindMap[normalizedKey]; !exists {
p.bindOrder = append(p.bindOrder, key)
}
p.bindMap[normalizedKey] = kb
}
func (p *MangoWCParser) ParseWithDMS() ([]MangoWCKeyBinding, error) {
expandedDir, err := utils.ExpandPath(p.configDir)
if err != nil {
return nil, err
}
dmsBindsPath := filepath.Join(expandedDir, "dms", "binds.conf")
if _, err := os.Stat(dmsBindsPath); err == nil {
p.dmsBindsExists = true
}
mainConfig := filepath.Join(expandedDir, "config.conf")
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
mainConfig = filepath.Join(expandedDir, "mango.conf")
}
_, err = p.parseFileWithSource(mainConfig)
if err != nil {
return nil, err
}
if p.dmsBindsExists && !p.dmsProcessed {
p.parseDMSBindsDirectly(dmsBindsPath)
}
var keybinds []MangoWCKeyBinding
for _, key := range p.bindOrder {
normalizedKey := p.normalizeKey(key)
if kb, exists := p.bindMap[normalizedKey]; exists {
keybinds = append(keybinds, *kb)
}
}
return keybinds, nil
}
func (p *MangoWCParser) parseFileWithSource(filePath string) ([]MangoWCKeyBinding, error) {
absPath, err := filepath.Abs(filePath)
if err != nil {
return nil, err
}
if p.processedFiles[absPath] {
return nil, nil
}
p.processedFiles[absPath] = true
data, err := os.ReadFile(absPath)
if err != nil {
return nil, err
}
prevSource := p.currentSource
p.currentSource = absPath
var keybinds []MangoWCKeyBinding
lines := strings.Split(string(data), "\n")
for lineNum, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "source") {
p.handleSource(trimmed, filepath.Dir(absPath), &keybinds)
continue
}
if !strings.HasPrefix(trimmed, "bind") {
continue
}
kb := p.getKeybindAtLineContent(line, lineNum)
if kb == nil {
continue
}
kb.Source = p.currentSource
p.addBind(kb)
keybinds = append(keybinds, *kb)
}
p.currentSource = prevSource
return keybinds, nil
}
func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKeyBinding) {
parts := strings.SplitN(line, "=", 2)
if len(parts) < 2 {
return
}
sourcePath := strings.TrimSpace(parts[1])
isDMSSource := sourcePath == "dms/binds.conf" || sourcePath == "./dms/binds.conf" || strings.HasSuffix(sourcePath, "/dms/binds.conf")
p.includeCount++
if isDMSSource {
p.dmsBindsIncluded = true
p.dmsIncludePos = p.includeCount
p.dmsProcessed = true
}
fullPath := sourcePath
if !filepath.IsAbs(sourcePath) {
fullPath = filepath.Join(baseDir, sourcePath)
}
expanded, err := utils.ExpandPath(fullPath)
if err != nil {
return
}
includedBinds, err := p.parseFileWithSource(expanded)
if err != nil {
return
}
*keybinds = append(*keybinds, includedBinds...)
}
func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyBinding {
data, err := os.ReadFile(dmsBindsPath)
if err != nil {
return nil
}
prevSource := p.currentSource
p.currentSource = dmsBindsPath
var keybinds []MangoWCKeyBinding
lines := strings.Split(string(data), "\n")
for lineNum, line := range lines {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "bind") {
continue
}
kb := p.getKeybindAtLineContent(line, lineNum)
if kb == nil {
continue
}
kb.Source = dmsBindsPath
p.addBind(kb)
keybinds = append(keybinds, *kb)
}
p.currentSource = prevSource
p.dmsProcessed = true
return keybinds
}
func (p *MangoWCParser) getKeybindAtLineContent(line string, _ int) *MangoWCKeyBinding {
bindMatch := regexp.MustCompile(`^(bind[lsr]*)\s*=\s*(.+)$`)
matches := bindMatch.FindStringSubmatch(line)
if len(matches) < 3 {
return nil
}
content := matches[2]
parts := strings.SplitN(content, "#", 2)
keys := parts[0]
var comment string
if len(parts) > 1 {
comment = strings.TrimSpace(parts[1])
}
if strings.HasPrefix(comment, MangoWCHideComment) {
return nil
}
keyFields := strings.SplitN(keys, ",", 4)
if len(keyFields) < 3 {
return nil
}
mods := strings.TrimSpace(keyFields[0])
key := strings.TrimSpace(keyFields[1])
command := strings.TrimSpace(keyFields[2])
var params string
if len(keyFields) > 3 {
params = strings.TrimSpace(keyFields[3])
}
if comment == "" {
comment = mangowcAutogenerateComment(command, params)
}
var modList []string
if mods != "" && !strings.EqualFold(mods, "none") {
modstring := mods + string(MangoWCModSeparators[0])
idx := 0
for index, char := range modstring {
isModSep := false
for _, sep := range MangoWCModSeparators {
if char == sep {
isModSep = true
break
}
}
if isModSep {
if index-idx > 1 {
modList = append(modList, modstring[idx:index])
}
idx = index + 1
}
}
}
return &MangoWCKeyBinding{
Mods: modList,
Key: key,
Command: command,
Params: params,
Comment: comment,
}
}
func ParseMangoWCKeysWithDMS(path string) (*MangoWCParseResult, error) {
parser := NewMangoWCParser(path)
keybinds, err := parser.ParseWithDMS()
if err != nil {
return nil, err
}
return &MangoWCParseResult{
Keybinds: keybinds,
DMSBindsIncluded: parser.dmsBindsIncluded,
DMSStatus: parser.buildDMSStatus(),
ConflictingConfigs: parser.conflictingConfigs,
}, nil
}

View File

@@ -172,7 +172,7 @@ func TestMangoWCGetKeybindAtLine(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
parser := NewMangoWCParser() parser := NewMangoWCParser("")
parser.contentLines = []string{tt.line} parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)
@@ -283,7 +283,7 @@ func TestMangoWCReadContentMultipleFiles(t *testing.T) {
t.Fatalf("Failed to write file2: %v", err) t.Fatalf("Failed to write file2: %v", err)
} }
parser := NewMangoWCParser() parser := NewMangoWCParser("")
if err := parser.ReadContent(tmpDir); err != nil { if err := parser.ReadContent(tmpDir); err != nil {
t.Fatalf("ReadContent failed: %v", err) t.Fatalf("ReadContent failed: %v", err)
} }
@@ -304,7 +304,7 @@ func TestMangoWCReadContentSingleFile(t *testing.T) {
t.Fatalf("Failed to write config: %v", err) t.Fatalf("Failed to write config: %v", err)
} }
parser := NewMangoWCParser() parser := NewMangoWCParser("")
if err := parser.ReadContent(configFile); err != nil { if err := parser.ReadContent(configFile); err != nil {
t.Fatalf("ReadContent failed: %v", err) t.Fatalf("ReadContent failed: %v", err)
} }
@@ -362,7 +362,7 @@ func TestMangoWCReadContentWithTildeExpansion(t *testing.T) {
t.Skip("Cannot create relative path") t.Skip("Cannot create relative path")
} }
parser := NewMangoWCParser() parser := NewMangoWCParser("")
tildePathMatch := "~/" + relPath tildePathMatch := "~/" + relPath
err = parser.ReadContent(tildePathMatch) err = parser.ReadContent(tildePathMatch)
@@ -419,7 +419,7 @@ func TestMangoWCInvalidBindLines(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
parser := NewMangoWCParser() parser := NewMangoWCParser("")
parser.contentLines = []string{tt.line} parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)

View File

@@ -15,8 +15,17 @@ func TestMangoWCProviderName(t *testing.T) {
func TestMangoWCProviderDefaultPath(t *testing.T) { func TestMangoWCProviderDefaultPath(t *testing.T) {
provider := NewMangoWCProvider("") provider := NewMangoWCProvider("")
if provider.configPath != "$HOME/.config/mango" { configDir, err := os.UserConfigDir()
t.Errorf("configPath = %q, want %q", provider.configPath, "$HOME/.config/mango") if err != nil {
// Fall back to testing for non-empty path
if provider.configPath == "" {
t.Error("configPath should not be empty")
}
return
}
expected := filepath.Join(configDir, "mango")
if provider.configPath != expected {
t.Errorf("configPath = %q, want %q", provider.configPath, expected)
} }
} }
@@ -174,7 +183,7 @@ func TestMangoWCConvertKeybind(t *testing.T) {
provider := NewMangoWCProvider("") provider := NewMangoWCProvider("")
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := provider.convertKeybind(tt.keybind) result := provider.convertKeybind(tt.keybind, nil)
if result.Key != tt.wantKey { if result.Key != tt.wantKey {
t.Errorf("convertKeybind().Key = %q, want %q", result.Key, tt.wantKey) t.Errorf("convertKeybind().Key = %q, want %q", result.Key, tt.wantKey)
} }

View File

@@ -187,7 +187,15 @@ func (n *NiriProvider) formatRawAction(action string, args []string) string {
} }
} }
return action + " " + strings.Join(args, " ") quotedArgs := make([]string, len(args))
for i, arg := range args {
if arg == "" {
quotedArgs[i] = `""`
} else {
quotedArgs[i] = arg
}
}
return action + " " + strings.Join(quotedArgs, " ")
} }
func (n *NiriProvider) formatKey(kb *NiriKeyBinding) string { func (n *NiriProvider) formatKey(kb *NiriKeyBinding) string {
@@ -293,9 +301,15 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
continue continue
} }
keyStr := parser.formatBindKey(kb) keyStr := parser.formatBindKey(kb)
action := n.buildActionFromNode(child)
if action == "" {
action = n.formatRawAction(kb.Action, kb.Args)
}
binds[keyStr] = &overrideBind{ binds[keyStr] = &overrideBind{
Key: keyStr, Key: keyStr,
Action: n.formatRawAction(kb.Action, kb.Args), Action: action,
Description: kb.Description, Description: kb.Description,
Options: n.extractOptions(child), Options: n.extractOptions(child),
} }
@@ -305,6 +319,42 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
return binds, nil return binds, nil
} }
func (n *NiriProvider) buildActionFromNode(bindNode *document.Node) string {
if len(bindNode.Children) == 0 {
return ""
}
actionNode := bindNode.Children[0]
actionName := actionNode.Name.String()
if actionName == "" {
return ""
}
parts := []string{actionName}
for _, arg := range actionNode.Arguments {
val := arg.ValueString()
if val == "" {
parts = append(parts, `""`)
} else {
parts = append(parts, val)
}
}
if actionNode.Properties != nil {
if val, ok := actionNode.Properties.Get("focus"); ok {
parts = append(parts, "focus="+val.String())
}
if val, ok := actionNode.Properties.Get("show-pointer"); ok {
parts = append(parts, "show-pointer="+val.String())
}
if val, ok := actionNode.Properties.Get("write-to-disk"); ok {
parts = append(parts, "write-to-disk="+val.String())
}
}
return strings.Join(parts, " ")
}
func (n *NiriProvider) extractOptions(node *document.Node) map[string]any { func (n *NiriProvider) extractOptions(node *document.Node) map[string]any {
if node.Properties == nil { if node.Properties == nil {
return make(map[string]any) return make(map[string]any)

View File

@@ -121,6 +121,8 @@ func TestNiriFormatRawAction(t *testing.T) {
}{ }{
{"spawn", []string{"kitty"}, "spawn kitty"}, {"spawn", []string{"kitty"}, "spawn kitty"},
{"spawn", []string{"dms", "ipc", "call"}, "spawn dms ipc call"}, {"spawn", []string{"dms", "ipc", "call"}, "spawn dms ipc call"},
{"spawn", []string{"dms", "ipc", "call", "brightness", "increment", "5", ""}, `spawn dms ipc call brightness increment 5 ""`},
{"spawn", []string{"dms", "ipc", "call", "dash", "toggle", ""}, `spawn dms ipc call dash toggle ""`},
{"close-window", nil, "close-window"}, {"close-window", nil, "close-window"},
{"fullscreen-window", nil, "fullscreen-window"}, {"fullscreen-window", nil, "fullscreen-window"},
{"focus-workspace", []string{"1"}, "focus-workspace 1"}, {"focus-workspace", []string{"1"}, "focus-workspace 1"},
@@ -324,6 +326,58 @@ func TestNiriGenerateBindsContentRoundTrip(t *testing.T) {
} }
} }
func TestNiriEmptyArgsPreservation(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"XF86MonBrightnessUp": {
Key: "XF86MonBrightnessUp",
Action: `spawn dms ipc call brightness increment 5 ""`,
Description: "Brightness Up",
},
"XF86MonBrightnessDown": {
Key: "XF86MonBrightnessDown",
Action: `spawn dms ipc call brightness decrement 5 ""`,
Description: "Brightness Down",
},
"Super+Alt+Page_Up": {
Key: "Super+Alt+Page_Up",
Action: `spawn dms ipc call dash toggle ""`,
Description: "Dashboard Toggle",
},
}
content := provider.generateBindsContent(binds)
tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(dmsDir, 0755); err != nil {
t.Fatalf("Failed to create dms directory: %v", err)
}
bindsFile := filepath.Join(dmsDir, "binds.kdl")
if err := os.WriteFile(bindsFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write binds file: %v", err)
}
testProvider := NewNiriProvider(tmpDir)
loadedBinds, err := testProvider.loadOverrideBinds()
if err != nil {
t.Fatalf("Failed to load binds: %v\nContent was:\n%s", err, content)
}
for key, expected := range binds {
loaded, ok := loadedBinds[key]
if !ok {
t.Errorf("Missing bind for key %s", key)
continue
}
if loaded.Action != expected.Action {
t.Errorf("Action mismatch for %s:\n got: %q\n want: %q", key, loaded.Action, expected.Action)
}
}
}
func TestNiriProviderWithRealWorldConfig(t *testing.T) { func TestNiriProviderWithRealWorldConfig(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl") configFile := filepath.Join(tmpDir, "config.kdl")

View File

@@ -8,6 +8,7 @@ type Keybind struct {
Source string `json:"source,omitempty"` Source string `json:"source,omitempty"`
HideOnOverlay bool `json:"hideOnOverlay,omitempty"` HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
CooldownMs int `json:"cooldownMs,omitempty"` CooldownMs int `json:"cooldownMs,omitempty"`
Flags string `json:"flags,omitempty"` // Hyprland bind flags: e=repeat, l=locked, r=release, o=long-press
Conflict *Keybind `json:"conflict,omitempty"` Conflict *Keybind `json:"conflict,omitempty"`
} }

View File

@@ -314,6 +314,7 @@ output_path = '%s'
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions"), opts.ShellDir) appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir) appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir) appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
default: default:
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile) appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
} }

View File

@@ -26,6 +26,7 @@ type Plugin struct {
Compositors []string `json:"compositors"` Compositors []string `json:"compositors"`
Distro []string `json:"distro"` Distro []string `json:"distro"`
Screenshot string `json:"screenshot,omitempty"` Screenshot string `json:"screenshot,omitempty"`
RequiresDMS string `json:"requires_dms,omitempty"`
} }
type GitClient interface { type GitClient interface {

View File

@@ -31,7 +31,7 @@ import (
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
// These mime types wont be stored in history // These mime types won't be stored in history
var sensitiveMimeTypes = []string{ var sensitiveMimeTypes = []string{
"x-kde-passwordManagerHint", "x-kde-passwordManagerHint",
} }

View File

@@ -3,6 +3,7 @@ package network
import ( import (
"bufio" "bufio"
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -925,25 +926,24 @@ func (b *NetworkManagerBackend) ImportVPN(filePath string, name string) (*VPNImp
func (b *NetworkManagerBackend) importVPNWithNmcli(filePath string, name string) (*VPNImportResult, error) { func (b *NetworkManagerBackend) importVPNWithNmcli(filePath string, name string) (*VPNImportResult, error) {
vpnTypes := []string{"openvpn", "wireguard", "vpnc", "pptp", "l2tp", "openconnect", "strongswan"} vpnTypes := []string{"openvpn", "wireguard", "vpnc", "pptp", "l2tp", "openconnect", "strongswan"}
var output []byte var allErrors []error
var err error var outputStr string
for _, vpnType := range vpnTypes { for _, vpnType := range vpnTypes {
args := []string{"connection", "import", "type", vpnType, "file", filePath} cmd := exec.Command("nmcli", "connection", "import", "type", vpnType, "file", filePath)
cmd := exec.Command("nmcli", args...) output, err := cmd.CombinedOutput()
output, err = cmd.CombinedOutput()
if err == nil { if err == nil {
outputStr = string(output)
break break
} }
allErrors = append(allErrors, fmt.Errorf("%s: %s", vpnType, strings.TrimSpace(string(output))))
} }
if err != nil { if len(allErrors) == len(vpnTypes) {
return &VPNImportResult{ return &VPNImportResult{
Success: false, Success: false,
Error: fmt.Sprintf("import failed: %s", strings.TrimSpace(string(output))), Error: errors.Join(allErrors...).Error(),
}, nil }, nil
} }
outputStr := string(output)
var connUUID, connName string var connUUID, connName string
lines := strings.Split(outputStr, "\n") lines := strings.Split(outputStr, "\n")

View File

@@ -357,31 +357,51 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
savedSSIDs := make(map[string]bool) savedSSIDs := make(map[string]bool)
autoconnectMap := make(map[string]bool) autoconnectMap := make(map[string]bool)
hiddenSSIDs := make(map[string]bool)
for _, conn := range connections { for _, conn := range connections {
connSettings, err := conn.GetSettings() connSettings, err := conn.GetSettings()
if err != nil { if err != nil {
continue continue
} }
if connMeta, ok := connSettings["connection"]; ok { connMeta, ok := connSettings["connection"]
if connType, ok := connMeta["type"].(string); ok && connType == "802-11-wireless" { if !ok {
if wifiSettings, ok := connSettings["802-11-wireless"]; ok { continue
if ssidBytes, ok := wifiSettings["ssid"].([]byte); ok { }
ssid := string(ssidBytes)
savedSSIDs[ssid] = true connType, ok := connMeta["type"].(string)
autoconnect := true if !ok || connType != "802-11-wireless" {
if ac, ok := connMeta["autoconnect"].(bool); ok { continue
autoconnect = ac }
}
autoconnectMap[ssid] = autoconnect wifiSettings, ok := connSettings["802-11-wireless"]
} if !ok {
} continue
} }
ssidBytes, ok := wifiSettings["ssid"].([]byte)
if !ok {
continue
}
ssid := string(ssidBytes)
savedSSIDs[ssid] = true
autoconnect := true
if ac, ok := connMeta["autoconnect"].(bool); ok {
autoconnect = ac
}
autoconnectMap[ssid] = autoconnect
if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden {
hiddenSSIDs[ssid] = true
} }
} }
b.stateMutex.RLock() b.stateMutex.RLock()
currentSSID := b.state.WiFiSSID currentSSID := b.state.WiFiSSID
wifiConnected := b.state.WiFiConnected
wifiSignal := b.state.WiFiSignal
wifiBSSID := b.state.WiFiBSSID
b.stateMutex.RUnlock() b.stateMutex.RUnlock()
seenSSIDs := make(map[string]*WiFiNetwork) seenSSIDs := make(map[string]*WiFiNetwork)
@@ -444,6 +464,7 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
Connected: ssid == currentSSID, Connected: ssid == currentSSID,
Saved: savedSSIDs[ssid], Saved: savedSSIDs[ssid],
Autoconnect: autoconnectMap[ssid], Autoconnect: autoconnectMap[ssid],
Hidden: hiddenSSIDs[ssid],
Frequency: freq, Frequency: freq,
Mode: modeStr, Mode: modeStr,
Rate: maxBitrate / 1000, Rate: maxBitrate / 1000,
@@ -454,6 +475,23 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
networks = append(networks, network) networks = append(networks, network)
} }
if wifiConnected && currentSSID != "" {
if _, exists := seenSSIDs[currentSSID]; !exists {
hiddenNetwork := WiFiNetwork{
SSID: currentSSID,
BSSID: wifiBSSID,
Signal: wifiSignal,
Secured: true,
Connected: true,
Saved: savedSSIDs[currentSSID],
Autoconnect: autoconnectMap[currentSSID],
Hidden: true,
Mode: "infrastructure",
}
networks = append(networks, hiddenNetwork)
}
}
sortWiFiNetworks(networks) sortWiFiNetworks(networks)
b.stateMutex.Lock() b.stateMutex.Lock()
@@ -515,40 +553,53 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
nm := b.nmConn.(gonetworkmanager.NetworkManager) nm := b.nmConn.(gonetworkmanager.NetworkManager)
dev := devInfo.device dev := devInfo.device
w := devInfo.wireless w := devInfo.wireless
apPaths, err := w.GetAccessPoints()
if err != nil {
return fmt.Errorf("failed to get access points: %w", err)
}
var targetAP gonetworkmanager.AccessPoint var targetAP gonetworkmanager.AccessPoint
for _, ap := range apPaths { var flags, wpaFlags, rsnFlags uint32
ssid, err := ap.GetPropertySSID()
if err != nil || ssid != req.SSID { if !req.Hidden {
continue apPaths, err := w.GetAccessPoints()
if err != nil {
return fmt.Errorf("failed to get access points: %w", err)
} }
targetAP = ap
break
}
if targetAP == nil { for _, ap := range apPaths {
return fmt.Errorf("access point not found: %s", req.SSID) ssid, err := ap.GetPropertySSID()
} if err != nil || ssid != req.SSID {
continue
}
targetAP = ap
break
}
flags, _ := targetAP.GetPropertyFlags() if targetAP == nil {
wpaFlags, _ := targetAP.GetPropertyWPAFlags() return fmt.Errorf("access point not found: %s", req.SSID)
rsnFlags, _ := targetAP.GetPropertyRSNFlags() }
flags, _ = targetAP.GetPropertyFlags()
wpaFlags, _ = targetAP.GetPropertyWPAFlags()
rsnFlags, _ = targetAP.GetPropertyRSNFlags()
}
const KeyMgmt8021x = uint32(512) const KeyMgmt8021x = uint32(512)
const KeyMgmtPsk = uint32(256) const KeyMgmtPsk = uint32(256)
const KeyMgmtSae = uint32(1024) const KeyMgmtSae = uint32(1024)
isEnterprise := (wpaFlags&KeyMgmt8021x) != 0 || (rsnFlags&KeyMgmt8021x) != 0 var isEnterprise, isPsk, isSae, secured bool
isPsk := (wpaFlags&KeyMgmtPsk) != 0 || (rsnFlags&KeyMgmtPsk) != 0
isSae := (wpaFlags&KeyMgmtSae) != 0 || (rsnFlags&KeyMgmtSae) != 0
secured := flags != uint32(gonetworkmanager.Nm80211APFlagsNone) || switch {
wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) || case req.Hidden:
rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone) secured = req.Password != "" || req.Username != ""
isEnterprise = req.Username != ""
isPsk = req.Password != "" && !isEnterprise
default:
isEnterprise = (wpaFlags&KeyMgmt8021x) != 0 || (rsnFlags&KeyMgmt8021x) != 0
isPsk = (wpaFlags&KeyMgmtPsk) != 0 || (rsnFlags&KeyMgmtPsk) != 0
isSae = (wpaFlags&KeyMgmtSae) != 0 || (rsnFlags&KeyMgmtSae) != 0
secured = flags != uint32(gonetworkmanager.Nm80211APFlagsNone) ||
wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) ||
rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone)
}
if isEnterprise { if isEnterprise {
log.Infof("[createAndConnectWiFi] Enterprise network detected (802.1x) - SSID: %s, interactive: %v", log.Infof("[createAndConnectWiFi] Enterprise network detected (802.1x) - SSID: %s, interactive: %v",
@@ -567,11 +618,15 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
settings["ipv6"] = map[string]any{"method": "auto"} settings["ipv6"] = map[string]any{"method": "auto"}
if secured { if secured {
settings["802-11-wireless"] = map[string]any{ wifiSettings := map[string]any{
"ssid": []byte(req.SSID), "ssid": []byte(req.SSID),
"mode": "infrastructure", "mode": "infrastructure",
"security": "802-11-wireless-security", "security": "802-11-wireless-security",
} }
if req.Hidden {
wifiSettings["hidden"] = true
}
settings["802-11-wireless"] = wifiSettings
switch { switch {
case isEnterprise || req.Username != "": case isEnterprise || req.Username != "":
@@ -658,10 +713,14 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
return fmt.Errorf("secured network but not SAE/PSK/802.1X (rsn=0x%x wpa=0x%x)", rsnFlags, wpaFlags) return fmt.Errorf("secured network but not SAE/PSK/802.1X (rsn=0x%x wpa=0x%x)", rsnFlags, wpaFlags)
} }
} else { } else {
settings["802-11-wireless"] = map[string]any{ wifiSettings := map[string]any{
"ssid": []byte(req.SSID), "ssid": []byte(req.SSID),
"mode": "infrastructure", "mode": "infrastructure",
} }
if req.Hidden {
wifiSettings["hidden"] = true
}
settings["802-11-wireless"] = wifiSettings
} }
if req.Interactive { if req.Interactive {
@@ -685,14 +744,23 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
log.Infof("[createAndConnectWiFi] Enterprise connection added, activating (secret agent will be called)") log.Infof("[createAndConnectWiFi] Enterprise connection added, activating (secret agent will be called)")
} }
_, err = nm.ActivateWirelessConnection(conn, dev, targetAP) if req.Hidden {
_, err = nm.ActivateConnection(conn, dev, nil)
} else {
_, err = nm.ActivateWirelessConnection(conn, dev, targetAP)
}
if err != nil { if err != nil {
return fmt.Errorf("failed to activate connection: %w", err) return fmt.Errorf("failed to activate connection: %w", err)
} }
log.Infof("[createAndConnectWiFi] Connection activation initiated, waiting for NetworkManager state changes...") log.Infof("[createAndConnectWiFi] Connection activation initiated, waiting for NetworkManager state changes...")
} else { } else {
_, err = nm.AddAndActivateWirelessConnection(settings, dev, targetAP) var err error
if req.Hidden {
_, err = nm.AddAndActivateConnection(settings, dev)
} else {
_, err = nm.AddAndActivateWirelessConnection(settings, dev, targetAP)
}
if err != nil { if err != nil {
return fmt.Errorf("failed to connect: %w", err) return fmt.Errorf("failed to connect: %w", err)
} }
@@ -813,6 +881,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
savedSSIDs := make(map[string]bool) savedSSIDs := make(map[string]bool)
autoconnectMap := make(map[string]bool) autoconnectMap := make(map[string]bool)
hiddenSSIDs := make(map[string]bool)
for _, conn := range connections { for _, conn := range connections {
connSettings, err := conn.GetSettings() connSettings, err := conn.GetSettings()
if err != nil { if err != nil {
@@ -846,6 +915,10 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
autoconnect = ac autoconnect = ac
} }
autoconnectMap[ssid] = autoconnect autoconnectMap[ssid] = autoconnect
if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden {
hiddenSSIDs[ssid] = true
}
} }
var devices []WiFiDevice var devices []WiFiDevice
@@ -939,6 +1012,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
Connected: connected && apSSID == ssid, Connected: connected && apSSID == ssid,
Saved: savedSSIDs[apSSID], Saved: savedSSIDs[apSSID],
Autoconnect: autoconnectMap[apSSID], Autoconnect: autoconnectMap[apSSID],
Hidden: hiddenSSIDs[apSSID],
Frequency: freq, Frequency: freq,
Mode: modeStr, Mode: modeStr,
Rate: maxBitrate / 1000, Rate: maxBitrate / 1000,
@@ -949,6 +1023,25 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
seenSSIDs[apSSID] = &network seenSSIDs[apSSID] = &network
networks = append(networks, network) networks = append(networks, network)
} }
if connected && ssid != "" {
if _, exists := seenSSIDs[ssid]; !exists {
hiddenNetwork := WiFiNetwork{
SSID: ssid,
BSSID: bssid,
Signal: signal,
Secured: true,
Connected: true,
Saved: savedSSIDs[ssid],
Autoconnect: autoconnectMap[ssid],
Hidden: true,
Mode: "infrastructure",
Device: name,
}
networks = append(networks, hiddenNetwork)
}
}
sortWiFiNetworks(networks) sortWiFiNetworks(networks)
} }

View File

@@ -33,6 +33,7 @@ type WiFiNetwork struct {
Connected bool `json:"connected"` Connected bool `json:"connected"`
Saved bool `json:"saved"` Saved bool `json:"saved"`
Autoconnect bool `json:"autoconnect"` Autoconnect bool `json:"autoconnect"`
Hidden bool `json:"hidden"`
Frequency uint32 `json:"frequency"` Frequency uint32 `json:"frequency"`
Mode string `json:"mode"` Mode string `json:"mode"`
Rate uint32 `json:"rate"` Rate uint32 `json:"rate"`
@@ -127,6 +128,7 @@ type ConnectionRequest struct {
AnonymousIdentity string `json:"anonymousIdentity,omitempty"` AnonymousIdentity string `json:"anonymousIdentity,omitempty"`
DomainSuffixMatch string `json:"domainSuffixMatch,omitempty"` DomainSuffixMatch string `json:"domainSuffixMatch,omitempty"`
Interactive bool `json:"interactive,omitempty"` Interactive bool `json:"interactive,omitempty"`
Hidden bool `json:"hidden,omitempty"`
Device string `json:"device,omitempty"` Device string `json:"device,omitempty"`
EAPMethod string `json:"eapMethod,omitempty"` EAPMethod string `json:"eapMethod,omitempty"`
Phase2Auth string `json:"phase2Auth,omitempty"` Phase2Auth string `json:"phase2Auth,omitempty"`

View File

@@ -44,6 +44,7 @@ func HandleList(conn net.Conn, req models.Request) {
Dependencies: p.Dependencies, Dependencies: p.Dependencies,
Installed: installed, Installed: installed,
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"), FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
RequiresDMS: p.RequiresDMS,
} }
} }

View File

@@ -60,6 +60,7 @@ func HandleListInstalled(conn net.Conn, req models.Request) {
Dependencies: plugin.Dependencies, Dependencies: plugin.Dependencies,
FirstParty: strings.HasPrefix(plugin.Repo, "https://github.com/AvengeMedia"), FirstParty: strings.HasPrefix(plugin.Repo, "https://github.com/AvengeMedia"),
HasUpdate: hasUpdate, HasUpdate: hasUpdate,
RequiresDMS: plugin.RequiresDMS,
}) })
} else { } else {
result = append(result, PluginInfo{ result = append(result, PluginInfo{

View File

@@ -66,6 +66,7 @@ func HandleSearch(conn net.Conn, req models.Request) {
Dependencies: p.Dependencies, Dependencies: p.Dependencies,
Installed: installed, Installed: installed,
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"), FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
RequiresDMS: p.RequiresDMS,
} }
} }

View File

@@ -15,6 +15,7 @@ type PluginInfo struct {
FirstParty bool `json:"firstParty,omitempty"` FirstParty bool `json:"firstParty,omitempty"`
Note string `json:"note,omitempty"` Note string `json:"note,omitempty"`
HasUpdate bool `json:"hasUpdate,omitempty"` HasUpdate bool `json:"hasUpdate,omitempty"`
RequiresDMS string `json:"requires_dms,omitempty"`
} }
type SuccessResult struct { type SuccessResult struct {

View File

@@ -124,27 +124,23 @@ func (sc *SharedContext) eventDispatcher() {
} }
for { for {
sc.drainCmdQueue()
select { select {
case <-sc.stopChan: case <-sc.stopChan:
return return
default: default:
} }
sc.drainCmdQueue() _, err := unix.Poll(pollFds, -1)
switch {
n, err := unix.Poll(pollFds, 50) case err == unix.EINTR:
if err != nil { continue
if err == unix.EINTR { case err != nil:
continue
}
log.Errorf("Poll error: %v", err) log.Errorf("Poll error: %v", err)
return return
} }
if n == 0 {
continue
}
if pollFds[1].Revents&unix.POLLIN != 0 { if pollFds[1].Revents&unix.POLLIN != 0 {
var buf [64]byte var buf [64]byte
if _, err := unix.Read(sc.wakeR, buf[:]); err != nil && err != unix.EAGAIN { if _, err := unix.Read(sc.wakeR, buf[:]); err != nil && err != unix.EAGAIN {
@@ -152,13 +148,13 @@ func (sc *SharedContext) eventDispatcher() {
} }
} }
if pollFds[0].Revents&unix.POLLIN != 0 { if pollFds[0].Revents&unix.POLLIN == 0 {
if err := ctx.Dispatch(); err != nil { continue
if !os.IsTimeout(err) { }
log.Errorf("Wayland connection error: %v", err)
return if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
} log.Errorf("Wayland connection error: %v", err)
} return
} }
} }
} }
@@ -176,12 +172,16 @@ func (sc *SharedContext) drainCmdQueue() {
func (sc *SharedContext) Close() { func (sc *SharedContext) Close() {
close(sc.stopChan) close(sc.stopChan)
if _, err := unix.Write(sc.wakeW, []byte{1}); err != nil && err != unix.EAGAIN {
log.Errorf("wake pipe write error on close: %v", err)
}
sc.wg.Wait() sc.wg.Wait()
unix.Close(sc.wakeR) unix.Close(sc.wakeR)
unix.Close(sc.wakeW) unix.Close(sc.wakeW)
if sc.display != nil { if sc.display == nil {
sc.display.Context().Close() return
} }
sc.display.Context().Close()
} }

View File

@@ -0,0 +1,20 @@
package utils
import (
"github.com/godbus/dbus/v5"
)
func IsDBusServiceAvailable(busName string) bool {
conn, err := dbus.ConnectSystemBus()
if err != nil {
return false
}
defer conn.Close()
obj := conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
var owned bool
if err := obj.Call("org.freedesktop.DBus.NameHasOwner", 0, busName).Store(&owned); err != nil {
return false
}
return owned
}

View File

@@ -2,7 +2,6 @@ package utils
import ( import (
"os/exec" "os/exec"
"strings"
) )
type AppChecker interface { type AppChecker interface {
@@ -43,16 +42,3 @@ func AnyCommandExists(cmds ...string) bool {
} }
return false return false
} }
func IsServiceActive(name string, userService bool) bool {
if !CommandExists("systemctl") {
return false
}
args := []string{"is-active", name}
if userService {
args = []string{"--user", "is-active", name}
}
output, _ := exec.Command("systemctl", args...).Output()
return strings.EqualFold(strings.TrimSpace(string(output)), "active")
}

View File

@@ -96,7 +96,7 @@ func (c *CUPSClient) RejectJobs(printer string) error {
return err return err
} }
// AddPrinterToClass adds a printer to a class, if the class does not exists it will be crated // AddPrinterToClass adds a printer to a class, if the class does not exists it will be created
func (c *CUPSClient) AddPrinterToClass(class, printer string) error { func (c *CUPSClient) AddPrinterToClass(class, printer string) error {
attributes, err := c.GetPrinterAttributes(class, []string{AttributeMemberURIs}) attributes, err := c.GetPrinterAttributes(class, []string{AttributeMemberURIs})
if err != nil && !IsNotExistsError(err) { if err != nil && !IsNotExistsError(err) {

View File

@@ -19,7 +19,8 @@ in
] ]
++ lib.optional cfg.enableDynamicTheming pkgs.matugen ++ lib.optional cfg.enableDynamicTheming pkgs.matugen
++ lib.optional cfg.enableAudioWavelength pkgs.cava ++ lib.optional cfg.enableAudioWavelength pkgs.cava
++ lib.optional cfg.enableCalendarEvents pkgs.khal; ++ lib.optional cfg.enableCalendarEvents pkgs.khal
++ lib.optional cfg.enableClipboardPaste pkgs.wtype;
plugins = lib.mapAttrs (name: plugin: { plugins = lib.mapAttrs (name: plugin: {
source = plugin.src; source = plugin.src;

View File

@@ -11,12 +11,18 @@ let
inherit (config.services.greetd.settings.default_session) user; inherit (config.services.greetd.settings.default_session) user;
compositorPackage =
let
configured = lib.attrByPath [ "programs" cfg.compositor.name "package" ] null config;
in
if configured != null then configured else builtins.getAttr cfg.compositor.name pkgs;
cacheDir = "/var/lib/dms-greeter"; cacheDir = "/var/lib/dms-greeter";
greeterScript = pkgs.writeShellScriptBin "dms-greeter" '' greeterScript = pkgs.writeShellScriptBin "dms-greeter" ''
export PATH=$PATH:${ export PATH=$PATH:${
lib.makeBinPath [ lib.makeBinPath [
cfg.quickshell.package cfg.quickshell.package
config.programs.${cfg.compositor.name}.package compositorPackage
] ]
} }
${ ${
@@ -64,6 +70,7 @@ in
"niri" "niri"
"hyprland" "hyprland"
"sway" "sway"
"labwc"
]; ];
description = "Compositor to run greeter in"; description = "Compositor to run greeter in";
}; };

View File

@@ -73,6 +73,13 @@ in
default = hasPluginSettings; default = hasPluginSettings;
description = ''Whether to manage plugin settings. Automatically enabled if any plugins have settings configured.''; description = ''Whether to manage plugin settings. Automatically enabled if any plugins have settings configured.'';
}; };
systemd.target = lib.mkOption {
type = lib.types.str;
default = config.wayland.systemd.target;
defaultText = lib.literalExpression "config.wayland.systemd.target";
description = "Systemd target to bind to.";
};
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
@@ -84,8 +91,8 @@ in
systemd.user.services.dms = lib.mkIf cfg.systemd.enable { systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
Unit = { Unit = {
Description = "DankMaterialShell"; Description = "DankMaterialShell";
PartOf = [ config.wayland.systemd.target ]; PartOf = [ cfg.systemd.target ];
After = [ config.wayland.systemd.target ]; After = [ cfg.systemd.target ];
}; };
Service = { Service = {
@@ -93,7 +100,7 @@ in
Restart = "on-failure"; Restart = "on-failure";
}; };
Install.WantedBy = [ config.wayland.systemd.target ]; Install.WantedBy = [ cfg.systemd.target ];
}; };
xdg.stateFile."DankMaterialShell/session.json" = lib.mkIf (cfg.session != { }) { xdg.stateFile."DankMaterialShell/session.json" = lib.mkIf (cfg.session != { }) {

View File

@@ -20,15 +20,19 @@ in
imports = [ imports = [
(import ./options.nix args) (import ./options.nix args)
]; ];
options.programs.dank-material-shell.systemd.target = lib.mkOption {
type = lib.types.str;
description = "Systemd target to bind to.";
default = "graphical-session.target";
};
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
systemd.user.services.dms = lib.mkIf cfg.systemd.enable { systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
description = "DankMaterialShell"; description = "DankMaterialShell";
path = lib.mkForce [ ]; path = lib.mkForce [ ];
partOf = [ "graphical-session.target" ]; partOf = [ cfg.systemd.target ];
after = [ "graphical-session.target" ]; after = [ cfg.systemd.target ];
wantedBy = [ "graphical-session.target" ]; wantedBy = [ cfg.systemd.target ];
restartIfChanged = cfg.systemd.restartIfChanged; restartIfChanged = cfg.systemd.restartIfChanged;
serviceConfig = { serviceConfig = {

View File

@@ -70,6 +70,12 @@ in
description = "Add calendar events support via khal"; description = "Add calendar events support via khal";
}; };
enableClipboardPaste = lib.mkOption {
type = types.bool;
default = true;
description = "Adds needed dependencies for directly pasting items from the clipboard history.";
};
quickshell = { quickshell = {
package = lib.mkPackageOption dmsPkgs "quickshell" { package = lib.mkPackageOption dmsPkgs "quickshell" {
extraDescription = "The quickshell package to use (defaults to be built from source, due to unreleased features used by DMS)."; extraDescription = "The quickshell package to use (defaults to be built from source, due to unreleased features used by DMS).";

View File

@@ -61,11 +61,13 @@
(builtins.substring 6 2 longDate) (builtins.substring 6 2 longDate)
]; ];
version = version =
pkgs.lib.removePrefix "v" (pkgs.lib.trim (builtins.readFile ./quickshell/VERSION)) let
+ "+date=" rawVersion = pkgs.lib.removePrefix "v" (pkgs.lib.trim (builtins.readFile ./quickshell/VERSION));
+ mkDate (self.lastModifiedDate or "19700101") cleanVersion = builtins.replaceStrings [ " " ] [ "" ] rawVersion;
+ "_" dateSuffix = "+date=" + mkDate (self.lastModifiedDate or "19700101");
+ (self.shortRev or "dirty"); revSuffix = "_" + (self.shortRev or "dirty");
in
"${cleanVersion}${dateSuffix}${revSuffix}";
in in
{ {
dms-shell = pkgs.buildGoModule ( dms-shell = pkgs.buildGoModule (
@@ -83,7 +85,7 @@
ldflags = [ ldflags = [
"-s" "-s"
"-w" "-w"
"-X main.Version=${version}" "-X 'main.Version=${version}'"
]; ];
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [

View File

@@ -46,7 +46,9 @@ const KEY_MAP = {
16777349: "XF86AudioMedia", 16777349: "XF86AudioMedia",
16777350: "XF86AudioRecord", 16777350: "XF86AudioRecord",
16842798: "XF86MonBrightnessUp", 16842798: "XF86MonBrightnessUp",
16777394: "XF86MonBrightnessUp",
16842797: "XF86MonBrightnessDown", 16842797: "XF86MonBrightnessDown",
16777395: "XF86MonBrightnessDown",
16842800: "XF86KbdBrightnessUp", 16842800: "XF86KbdBrightnessUp",
16842799: "XF86KbdBrightnessDown", 16842799: "XF86KbdBrightnessDown",
16842796: "XF86PowerOff", 16842796: "XF86PowerOff",

View File

@@ -103,7 +103,7 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call wallpaper prev", label: "Wallpaper: Previous" } { id: "spawn dms ipc call wallpaper prev", label: "Wallpaper: Previous" }
]; ];
const COMPOSITOR_ACTIONS = { const NIRI_ACTIONS = {
"Window": [ "Window": [
{ id: "close-window", label: "Close Window" }, { id: "close-window", label: "Close Window" },
{ id: "fullscreen-window", label: "Fullscreen" }, { id: "fullscreen-window", label: "Fullscreen" },
@@ -179,9 +179,246 @@ const COMPOSITOR_ACTIONS = {
] ]
}; };
const CATEGORY_ORDER = ["DMS", "Execute", "Workspace", "Window", "Monitor", "Screenshot", "System", "Overview", "Alt-Tab", "Other"]; const MANGOWC_ACTIONS = {
"Window": [
{ id: "killclient", label: "Close Window" },
{ id: "focuslast", label: "Focus Last Window" },
{ id: "focusstack next", label: "Focus Next in Stack" },
{ id: "focusstack prev", label: "Focus Previous in Stack" },
{ id: "focusdir left", label: "Focus Left" },
{ id: "focusdir right", label: "Focus Right" },
{ id: "focusdir up", label: "Focus Up" },
{ id: "focusdir down", label: "Focus Down" },
{ id: "exchange_client left", label: "Swap Left" },
{ id: "exchange_client right", label: "Swap Right" },
{ id: "exchange_client up", label: "Swap Up" },
{ id: "exchange_client down", label: "Swap Down" },
{ id: "exchange_stack_client next", label: "Swap Next in Stack" },
{ id: "exchange_stack_client prev", label: "Swap Previous in Stack" },
{ id: "togglefloating", label: "Toggle Floating" },
{ id: "togglefullscreen", label: "Toggle Fullscreen" },
{ id: "togglefakefullscreen", label: "Toggle Fake Fullscreen" },
{ id: "togglemaximizescreen", label: "Toggle Maximize" },
{ id: "toggleglobal", label: "Toggle Global (Sticky)" },
{ id: "toggleoverlay", label: "Toggle Overlay" },
{ id: "minimized", label: "Minimize Window" },
{ id: "restore_minimized", label: "Restore Minimized" },
{ id: "toggle_render_border", label: "Toggle Border" },
{ id: "centerwin", label: "Center Window" },
{ id: "zoom", label: "Swap with Master" }
],
"Move/Resize": [
{ id: "smartmovewin left", label: "Smart Move Left" },
{ id: "smartmovewin right", label: "Smart Move Right" },
{ id: "smartmovewin up", label: "Smart Move Up" },
{ id: "smartmovewin down", label: "Smart Move Down" },
{ id: "smartresizewin left", label: "Smart Resize Left" },
{ id: "smartresizewin right", label: "Smart Resize Right" },
{ id: "smartresizewin up", label: "Smart Resize Up" },
{ id: "smartresizewin down", label: "Smart Resize Down" },
{ id: "movewin", label: "Move Window (x,y)" },
{ id: "resizewin", label: "Resize Window (w,h)" }
],
"Tags": [
{ id: "view", label: "View Tag" },
{ id: "viewtoleft", label: "View Left Tag" },
{ id: "viewtoright", label: "View Right Tag" },
{ id: "viewtoleft_have_client", label: "View Left (with client)" },
{ id: "viewtoright_have_client", label: "View Right (with client)" },
{ id: "viewcrossmon", label: "View Cross-Monitor" },
{ id: "tag", label: "Move to Tag" },
{ id: "tagsilent", label: "Move to Tag (silent)" },
{ id: "tagtoleft", label: "Move to Left Tag" },
{ id: "tagtoright", label: "Move to Right Tag" },
{ id: "tagcrossmon", label: "Move Cross-Monitor" },
{ id: "toggletag", label: "Toggle Tag on Window" },
{ id: "toggleview", label: "Toggle Tag View" },
{ id: "comboview", label: "Combo View Tags" }
],
"Layout": [
{ id: "setlayout", label: "Set Layout" },
{ id: "switch_layout", label: "Cycle Layouts" },
{ id: "set_proportion", label: "Set Proportion" },
{ id: "switch_proportion_preset", label: "Cycle Proportion Presets" },
{ id: "incnmaster +1", label: "Increase Masters" },
{ id: "incnmaster -1", label: "Decrease Masters" },
{ id: "setmfact", label: "Set Master Factor" },
{ id: "incgaps", label: "Adjust Gaps" },
{ id: "togglegaps", label: "Toggle Gaps" }
],
"Monitor": [
{ id: "focusmon left", label: "Focus Monitor Left" },
{ id: "focusmon right", label: "Focus Monitor Right" },
{ id: "focusmon up", label: "Focus Monitor Up" },
{ id: "focusmon down", label: "Focus Monitor Down" },
{ id: "tagmon left", label: "Move to Monitor Left" },
{ id: "tagmon right", label: "Move to Monitor Right" },
{ id: "tagmon up", label: "Move to Monitor Up" },
{ id: "tagmon down", label: "Move to Monitor Down" },
{ id: "disable_monitor", label: "Disable Monitor" },
{ id: "enable_monitor", label: "Enable Monitor" },
{ id: "toggle_monitor", label: "Toggle Monitor" },
{ id: "create_virtual_output", label: "Create Virtual Output" },
{ id: "destroy_all_virtual_output", label: "Destroy Virtual Outputs" }
],
"Scratchpad": [
{ id: "toggle_scratchpad", label: "Toggle Scratchpad" },
{ id: "toggle_name_scratchpad", label: "Toggle Named Scratchpad" }
],
"Overview": [
{ id: "toggleoverview", label: "Toggle Overview" }
],
"System": [
{ id: "reload_config", label: "Reload Config" },
{ id: "quit", label: "Quit MangoWC" },
{ id: "setkeymode", label: "Set Keymode" },
{ id: "switch_keyboard_layout", label: "Switch Keyboard Layout" },
{ id: "setoption", label: "Set Option" },
{ id: "toggle_trackpad_enable", label: "Toggle Trackpad" }
]
};
const ACTION_ARGS = { const HYPRLAND_ACTIONS = {
"Window": [
{ id: "killactive", label: "Close Window" },
{ id: "forcekillactive", label: "Force Kill Window" },
{ id: "closewindow", label: "Close Window (by selector)" },
{ id: "killwindow", label: "Kill Window (by selector)" },
{ id: "togglefloating", label: "Toggle Floating" },
{ id: "setfloating", label: "Set Floating" },
{ id: "settiled", label: "Set Tiled" },
{ id: "fullscreen", label: "Toggle Fullscreen" },
{ id: "fullscreenstate", label: "Set Fullscreen State" },
{ id: "pin", label: "Pin Window" },
{ id: "centerwindow", label: "Center Window" },
{ id: "resizeactive", label: "Resize Active Window" },
{ id: "moveactive", label: "Move Active Window" },
{ id: "resizewindowpixel", label: "Resize Window (pixels)" },
{ id: "movewindowpixel", label: "Move Window (pixels)" },
{ id: "alterzorder", label: "Change Z-Order" },
{ id: "bringactivetotop", label: "Bring to Top" },
{ id: "setprop", label: "Set Window Property" },
{ id: "toggleswallow", label: "Toggle Swallow" }
],
"Focus": [
{ id: "movefocus l", label: "Focus Left" },
{ id: "movefocus r", label: "Focus Right" },
{ id: "movefocus u", label: "Focus Up" },
{ id: "movefocus d", label: "Focus Down" },
{ id: "movefocus", label: "Move Focus (direction)" },
{ id: "cyclenext", label: "Cycle Next Window" },
{ id: "cyclenext prev", label: "Cycle Previous Window" },
{ id: "focuswindow", label: "Focus Window (by selector)" },
{ id: "focuscurrentorlast", label: "Focus Current or Last" },
{ id: "focusurgentorlast", label: "Focus Urgent or Last" }
],
"Move": [
{ id: "movewindow l", label: "Move Window Left" },
{ id: "movewindow r", label: "Move Window Right" },
{ id: "movewindow u", label: "Move Window Up" },
{ id: "movewindow d", label: "Move Window Down" },
{ id: "movewindow", label: "Move Window (direction)" },
{ id: "swapwindow l", label: "Swap Left" },
{ id: "swapwindow r", label: "Swap Right" },
{ id: "swapwindow u", label: "Swap Up" },
{ id: "swapwindow d", label: "Swap Down" },
{ id: "swapwindow", label: "Swap Window (direction)" },
{ id: "swapnext", label: "Swap with Next" },
{ id: "swapnext prev", label: "Swap with Previous" },
{ id: "movecursortocorner", label: "Move Cursor to Corner" },
{ id: "movecursor", label: "Move Cursor (x,y)" }
],
"Workspace": [
{ id: "workspace", label: "Focus Workspace" },
{ id: "workspace +1", label: "Next Workspace" },
{ id: "workspace -1", label: "Previous Workspace" },
{ id: "workspace e+1", label: "Next Open Workspace" },
{ id: "workspace e-1", label: "Previous Open Workspace" },
{ id: "workspace previous", label: "Previous Visited Workspace" },
{ id: "workspace previous_per_monitor", label: "Previous on Monitor" },
{ id: "workspace empty", label: "First Empty Workspace" },
{ id: "movetoworkspace", label: "Move to Workspace" },
{ id: "movetoworkspace +1", label: "Move to Next Workspace" },
{ id: "movetoworkspace -1", label: "Move to Previous Workspace" },
{ id: "movetoworkspacesilent", label: "Move to Workspace (silent)" },
{ id: "movetoworkspacesilent +1", label: "Move to Next (silent)" },
{ id: "movetoworkspacesilent -1", label: "Move to Previous (silent)" },
{ id: "togglespecialworkspace", label: "Toggle Special Workspace" },
{ id: "focusworkspaceoncurrentmonitor", label: "Focus Workspace on Current Monitor" },
{ id: "renameworkspace", label: "Rename Workspace" }
],
"Monitor": [
{ id: "focusmonitor l", label: "Focus Monitor Left" },
{ id: "focusmonitor r", label: "Focus Monitor Right" },
{ id: "focusmonitor u", label: "Focus Monitor Up" },
{ id: "focusmonitor d", label: "Focus Monitor Down" },
{ id: "focusmonitor +1", label: "Focus Next Monitor" },
{ id: "focusmonitor -1", label: "Focus Previous Monitor" },
{ id: "focusmonitor", label: "Focus Monitor (by selector)" },
{ id: "movecurrentworkspacetomonitor", label: "Move Workspace to Monitor" },
{ id: "moveworkspacetomonitor", label: "Move Specific Workspace to Monitor" },
{ id: "swapactiveworkspaces", label: "Swap Active Workspaces" }
],
"Groups": [
{ id: "togglegroup", label: "Toggle Group" },
{ id: "changegroupactive f", label: "Next in Group" },
{ id: "changegroupactive b", label: "Previous in Group" },
{ id: "changegroupactive", label: "Change Active in Group" },
{ id: "moveintogroup l", label: "Move into Group Left" },
{ id: "moveintogroup r", label: "Move into Group Right" },
{ id: "moveintogroup u", label: "Move into Group Up" },
{ id: "moveintogroup d", label: "Move into Group Down" },
{ id: "moveoutofgroup", label: "Move out of Group" },
{ id: "movewindoworgroup l", label: "Move Window/Group Left" },
{ id: "movewindoworgroup r", label: "Move Window/Group Right" },
{ id: "movewindoworgroup u", label: "Move Window/Group Up" },
{ id: "movewindoworgroup d", label: "Move Window/Group Down" },
{ id: "movegroupwindow f", label: "Swap Forward in Group" },
{ id: "movegroupwindow b", label: "Swap Backward in Group" },
{ id: "lockgroups lock", label: "Lock All Groups" },
{ id: "lockgroups unlock", label: "Unlock All Groups" },
{ id: "lockgroups toggle", label: "Toggle Groups Lock" },
{ id: "lockactivegroup lock", label: "Lock Active Group" },
{ id: "lockactivegroup unlock", label: "Unlock Active Group" },
{ id: "lockactivegroup toggle", label: "Toggle Active Group Lock" },
{ id: "denywindowfromgroup on", label: "Deny Window from Group" },
{ id: "denywindowfromgroup off", label: "Allow Window in Group" },
{ id: "denywindowfromgroup toggle", label: "Toggle Deny from Group" },
{ id: "setignoregrouplock on", label: "Ignore Group Lock" },
{ id: "setignoregrouplock off", label: "Respect Group Lock" },
{ id: "setignoregrouplock toggle", label: "Toggle Ignore Group Lock" }
],
"Layout": [
{ id: "splitratio", label: "Adjust Split Ratio" }
],
"System": [
{ id: "exit", label: "Exit Hyprland" },
{ id: "forcerendererreload", label: "Force Renderer Reload" },
{ id: "dpms on", label: "DPMS On" },
{ id: "dpms off", label: "DPMS Off" },
{ id: "dpms toggle", label: "DPMS Toggle" },
{ id: "forceidle", label: "Force Idle" },
{ id: "submap", label: "Enter Submap" },
{ id: "submap reset", label: "Reset Submap" },
{ id: "global", label: "Global Shortcut" },
{ id: "event", label: "Emit Custom Event" }
],
"Pass-through": [
{ id: "pass", label: "Pass Key to Window" },
{ id: "sendshortcut", label: "Send Shortcut to Window" },
{ id: "sendkeystate", label: "Send Key State" }
]
};
const COMPOSITOR_ACTIONS = {
niri: NIRI_ACTIONS,
mangowc: MANGOWC_ACTIONS,
hyprland: HYPRLAND_ACTIONS
};
const CATEGORY_ORDER = ["DMS", "Execute", "Workspace", "Tags", "Window", "Move/Resize", "Focus", "Move", "Layout", "Groups", "Monitor", "Scratchpad", "Screenshot", "System", "Pass-through", "Overview", "Alt-Tab", "Other"];
const NIRI_ACTION_ARGS = {
"set-column-width": { "set-column-width": {
args: [{ name: "value", type: "text", label: "Width", placeholder: "+10%, -10%, 50%" }] args: [{ name: "value", type: "text", label: "Width", placeholder: "+10%, -10%, 50%" }]
}, },
@@ -213,13 +450,257 @@ const ACTION_ARGS = {
] ]
}, },
"screenshot-window": { "screenshot-window": {
args: [ args: [{ name: "write-to-disk", type: "bool", label: "Save to disk" }]
{ name: "show-pointer", type: "bool", label: "Show pointer" },
{ name: "write-to-disk", type: "bool", label: "Save to disk" }
]
} }
}; };
const MANGOWC_ACTION_ARGS = {
"view": {
args: [
{ name: "tag", type: "number", label: "Tag", placeholder: "1-9" },
{ name: "monitor", type: "number", label: "Monitor", placeholder: "0", default: "0" }
]
},
"tag": {
args: [
{ name: "tag", type: "number", label: "Tag", placeholder: "1-9" },
{ name: "monitor", type: "number", label: "Monitor", placeholder: "0", default: "0" }
]
},
"tagsilent": {
args: [
{ name: "tag", type: "number", label: "Tag", placeholder: "1-9" },
{ name: "monitor", type: "number", label: "Monitor", placeholder: "0", default: "0" }
]
},
"toggletag": {
args: [
{ name: "tag", type: "number", label: "Tag", placeholder: "1-9" },
{ name: "monitor", type: "number", label: "Monitor", placeholder: "0", default: "0" }
]
},
"toggleview": {
args: [
{ name: "tag", type: "number", label: "Tag", placeholder: "1-9" },
{ name: "monitor", type: "number", label: "Monitor", placeholder: "0", default: "0" }
]
},
"comboview": {
args: [{ name: "tags", type: "text", label: "Tags", placeholder: "1,2,3" }]
},
"setlayout": {
args: [{ name: "layout", type: "text", label: "Layout", placeholder: "tile, monocle, grid, deck" }]
},
"set_proportion": {
args: [{ name: "value", type: "text", label: "Proportion", placeholder: "0.5, +0.1, -0.1" }]
},
"setmfact": {
args: [{ name: "value", type: "text", label: "Factor", placeholder: "+0.05, -0.05" }]
},
"incgaps": {
args: [{ name: "value", type: "number", label: "Amount", placeholder: "+5, -5" }]
},
"movewin": {
args: [{ name: "value", type: "text", label: "Position", placeholder: "x,y or +10,+10" }]
},
"resizewin": {
args: [{ name: "value", type: "text", label: "Size", placeholder: "w,h or +10,+10" }]
},
"setkeymode": {
args: [{ name: "mode", type: "text", label: "Mode", placeholder: "default, custom" }]
},
"setoption": {
args: [{ name: "option", type: "text", label: "Option", placeholder: "option_name value" }]
},
"toggle_name_scratchpad": {
args: [{ name: "name", type: "text", label: "Name", placeholder: "scratchpad name" }]
},
"incnmaster": {
args: [{ name: "value", type: "number", label: "Amount", placeholder: "+1, -1" }]
}
};
const HYPRLAND_ACTION_ARGS = {
"workspace": {
args: [{ name: "value", type: "text", label: "Workspace", placeholder: "1, +1, -1, name:..." }]
},
"movetoworkspace": {
args: [
{ name: "workspace", type: "text", label: "Workspace", placeholder: "1, +1, special:name" },
{ name: "window", type: "text", label: "Window (optional)", placeholder: "class:^(app)$" }
]
},
"movetoworkspacesilent": {
args: [
{ name: "workspace", type: "text", label: "Workspace", placeholder: "1, +1, special:name" },
{ name: "window", type: "text", label: "Window (optional)", placeholder: "class:^(app)$" }
]
},
"focusworkspaceoncurrentmonitor": {
args: [{ name: "value", type: "text", label: "Workspace", placeholder: "1, +1, name:..." }]
},
"togglespecialworkspace": {
args: [{ name: "name", type: "text", label: "Name (optional)", placeholder: "scratchpad" }]
},
"focusmonitor": {
args: [{ name: "value", type: "text", label: "Monitor", placeholder: "l, r, +1, DP-1" }]
},
"movecurrentworkspacetomonitor": {
args: [{ name: "monitor", type: "text", label: "Monitor", placeholder: "l, r, DP-1" }]
},
"moveworkspacetomonitor": {
args: [
{ name: "workspace", type: "text", label: "Workspace", placeholder: "1, name:..." },
{ name: "monitor", type: "text", label: "Monitor", placeholder: "DP-1" }
]
},
"swapactiveworkspaces": {
args: [
{ name: "monitor1", type: "text", label: "Monitor 1", placeholder: "DP-1" },
{ name: "monitor2", type: "text", label: "Monitor 2", placeholder: "DP-2" }
]
},
"renameworkspace": {
args: [
{ name: "id", type: "number", label: "Workspace ID", placeholder: "1" },
{ name: "name", type: "text", label: "New Name", placeholder: "work" }
]
},
"fullscreen": {
args: [{ name: "mode", type: "text", label: "Mode", placeholder: "0=full, 1=max, 2=fake" }]
},
"fullscreenstate": {
args: [
{ name: "internal", type: "text", label: "Internal", placeholder: "-1, 0, 1, 2, 3" },
{ name: "client", type: "text", label: "Client", placeholder: "-1, 0, 1, 2, 3" }
]
},
"resizeactive": {
args: [{ name: "value", type: "text", label: "Size", placeholder: "10 -10, 20% 0" }]
},
"moveactive": {
args: [{ name: "value", type: "text", label: "Position", placeholder: "10 -10, exact 100 100" }]
},
"resizewindowpixel": {
args: [
{ name: "size", type: "text", label: "Size", placeholder: "100 100" },
{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" }
]
},
"movewindowpixel": {
args: [
{ name: "position", type: "text", label: "Position", placeholder: "100 100" },
{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" }
]
},
"splitratio": {
args: [{ name: "value", type: "text", label: "Ratio", placeholder: "+0.1, -0.1, exact 0.5" }]
},
"closewindow": {
args: [{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" }]
},
"killwindow": {
args: [{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" }]
},
"focuswindow": {
args: [{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" }]
},
"tagwindow": {
args: [
{ name: "tag", type: "text", label: "Tag", placeholder: "+mytag, -mytag" },
{ name: "window", type: "text", label: "Window (optional)", placeholder: "class:^(app)$" }
]
},
"alterzorder": {
args: [
{ name: "zheight", type: "text", label: "Z-Height", placeholder: "top, bottom" },
{ name: "window", type: "text", label: "Window (optional)", placeholder: "class:^(app)$" }
]
},
"setprop": {
args: [
{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" },
{ name: "property", type: "text", label: "Property", placeholder: "opaque, alpha..." },
{ name: "value", type: "text", label: "Value", placeholder: "1, toggle" }
]
},
"signal": {
args: [{ name: "signal", type: "number", label: "Signal", placeholder: "9" }]
},
"signalwindow": {
args: [
{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" },
{ name: "signal", type: "number", label: "Signal", placeholder: "9" }
]
},
"submap": {
args: [{ name: "name", type: "text", label: "Submap Name", placeholder: "resize, reset" }]
},
"global": {
args: [{ name: "name", type: "text", label: "Shortcut Name", placeholder: "app:action" }]
},
"event": {
args: [{ name: "data", type: "text", label: "Event Data", placeholder: "custom data" }]
},
"pass": {
args: [{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" }]
},
"sendshortcut": {
args: [
{ name: "mod", type: "text", label: "Modifier", placeholder: "SUPER, ALT" },
{ name: "key", type: "text", label: "Key", placeholder: "F4" },
{ name: "window", type: "text", label: "Window (optional)", placeholder: "class:^(app)$" }
]
},
"sendkeystate": {
args: [
{ name: "mod", type: "text", label: "Modifier", placeholder: "SUPER" },
{ name: "key", type: "text", label: "Key", placeholder: "a" },
{ name: "state", type: "text", label: "State", placeholder: "down, repeat, up" },
{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" }
]
},
"forceidle": {
args: [{ name: "seconds", type: "number", label: "Seconds", placeholder: "300" }]
},
"movecursortocorner": {
args: [{ name: "corner", type: "number", label: "Corner", placeholder: "0-3 (BL, BR, TR, TL)" }]
},
"movecursor": {
args: [
{ name: "x", type: "number", label: "X", placeholder: "100" },
{ name: "y", type: "number", label: "Y", placeholder: "100" }
]
},
"changegroupactive": {
args: [{ name: "direction", type: "text", label: "Direction/Index", placeholder: "f, b, or index" }]
},
"movefocus": {
args: [{ name: "direction", type: "text", label: "Direction", placeholder: "l, r, u, d" }]
},
"movewindow": {
args: [{ name: "direction", type: "text", label: "Direction/Monitor", placeholder: "l, r, mon:DP-1" }]
},
"swapwindow": {
args: [{ name: "direction", type: "text", label: "Direction", placeholder: "l, r, u, d" }]
},
"moveintogroup": {
args: [{ name: "direction", type: "text", label: "Direction", placeholder: "l, r, u, d" }]
},
"movewindoworgroup": {
args: [{ name: "direction", type: "text", label: "Direction", placeholder: "l, r, u, d" }]
},
"cyclenext": {
args: [{ name: "options", type: "text", label: "Options", placeholder: "prev, tiled, floating" }]
}
};
const ACTION_ARGS = {
niri: NIRI_ACTION_ARGS,
mangowc: MANGOWC_ACTION_ARGS,
hyprland: HYPRLAND_ACTION_ARGS
};
const DMS_ACTION_ARGS = { const DMS_ACTION_ARGS = {
"audio increment": { "audio increment": {
base: "spawn dms ipc call audio increment", base: "spawn dms ipc call audio increment",
@@ -287,12 +768,18 @@ function getDmsActions(isNiri, isHyprland) {
return result; return result;
} }
function getCompositorCategories() { function getCompositorCategories(compositor) {
return Object.keys(COMPOSITOR_ACTIONS); var actions = COMPOSITOR_ACTIONS[compositor];
if (!actions)
return [];
return Object.keys(actions);
} }
function getCompositorActions(category) { function getCompositorActions(compositor, category) {
return COMPOSITOR_ACTIONS[category] || []; var actions = COMPOSITOR_ACTIONS[compositor];
if (!actions)
return [];
return actions[category] || [];
} }
function getCategoryOrder() { function getCategoryOrder() {
@@ -307,9 +794,12 @@ function findDmsAction(actionId) {
return null; return null;
} }
function findCompositorAction(actionId) { function findCompositorAction(compositor, actionId) {
for (const cat in COMPOSITOR_ACTIONS) { var actions = COMPOSITOR_ACTIONS[compositor];
const acts = COMPOSITOR_ACTIONS[cat]; if (!actions)
return null;
for (const cat in actions) {
const acts = actions[cat];
for (let i = 0; i < acts.length; i++) { for (let i = 0; i < acts.length; i++) {
if (acts[i].id === actionId) if (acts[i].id === actionId)
return acts[i]; return acts[i];
@@ -318,7 +808,7 @@ function findCompositorAction(actionId) {
return null; return null;
} }
function getActionLabel(action) { function getActionLabel(action, compositor) {
if (!action) if (!action)
return ""; return "";
@@ -326,10 +816,15 @@ function getActionLabel(action) {
if (dmsAct) if (dmsAct)
return dmsAct.label; return dmsAct.label;
var base = action.split(" ")[0]; if (compositor) {
var compAct = findCompositorAction(base); var compAct = findCompositorAction(compositor, action);
if (compAct) if (compAct)
return compAct.label; return compAct.label;
var base = action.split(" ")[0];
compAct = findCompositorAction(compositor, base);
if (compAct)
return compAct.label;
}
if (action.startsWith("spawn sh -c ")) if (action.startsWith("spawn sh -c "))
return action.slice(12).replace(/^["']|["']$/g, ""); return action.slice(12).replace(/^["']|["']$/g, "");
@@ -343,7 +838,7 @@ function getActionType(action) {
return "compositor"; return "compositor";
if (action.startsWith("spawn dms ipc call ")) if (action.startsWith("spawn dms ipc call "))
return "dms"; return "dms";
if (action.startsWith("spawn sh -c ") || action.startsWith("spawn bash -c ")) if (/^spawn \w+ -c /.test(action) || action.startsWith("spawn_shell "))
return "shell"; return "shell";
if (action.startsWith("spawn ")) if (action.startsWith("spawn "))
return "spawn"; return "spawn";
@@ -364,16 +859,21 @@ function isValidAction(action) {
case "spawn ": case "spawn ":
case "spawn sh -c \"\"": case "spawn sh -c \"\"":
case "spawn sh -c ''": case "spawn sh -c ''":
case "spawn_shell":
case "spawn_shell ":
return false; return false;
} }
return true; return true;
} }
function isKnownCompositorAction(action) { function isKnownCompositorAction(compositor, action) {
if (!action) if (!action || !compositor)
return false; return false;
var found = findCompositorAction(compositor, action);
if (found)
return true;
var base = action.split(" ")[0]; var base = action.split(" ")[0];
return findCompositorAction(base) !== null; return findCompositorAction(compositor, base) !== null;
} }
function buildSpawnAction(command, args) { function buildSpawnAction(command, args) {
@@ -385,10 +885,13 @@ function buildSpawnAction(command, args) {
return "spawn " + parts.join(" "); return "spawn " + parts.join(" ");
} }
function buildShellAction(shellCmd) { function buildShellAction(compositor, shellCmd, shell) {
if (!shellCmd) if (!shellCmd)
return ""; return "";
return "spawn sh -c \"" + shellCmd.replace(/"/g, "\\\"") + "\""; if (compositor === "mangowc")
return "spawn_shell " + shellCmd;
var shellBin = shell || "sh";
return "spawn " + shellBin + " -c \"" + shellCmd.replace(/"/g, "\\\"") + "\"";
} }
function parseSpawnCommand(action) { function parseSpawnCommand(action) {
@@ -405,21 +908,33 @@ function parseSpawnCommand(action) {
function parseShellCommand(action) { function parseShellCommand(action) {
if (!action) if (!action)
return ""; return "";
if (!action.startsWith("spawn sh -c ")) var match = action.match(/^spawn (\w+) -c (.+)$/);
return ""; if (match) {
var content = action.slice(12); var content = match[2];
if ((content.startsWith('"') && content.endsWith('"')) || (content.startsWith("'") && content.endsWith("'"))) if ((content.startsWith('"') && content.endsWith('"')) || (content.startsWith("'") && content.endsWith("'")))
content = content.slice(1, -1); content = content.slice(1, -1);
return content.replace(/\\"/g, "\""); return content.replace(/\\"/g, "\"");
}
if (action.startsWith("spawn_shell "))
return action.slice(12);
return "";
} }
function getActionArgConfig(action) { function getShellFromAction(action) {
if (!action)
return "sh";
var match = action.match(/^spawn (\w+) -c /);
return match ? match[1] : "sh";
}
function getActionArgConfig(compositor, action) {
if (!action) if (!action)
return null; return null;
var baseAction = action.split(" ")[0]; var baseAction = action.split(" ")[0];
if (ACTION_ARGS[baseAction]) var compositorArgs = ACTION_ARGS[compositor];
return { type: "compositor", base: baseAction, config: ACTION_ARGS[baseAction] }; if (compositorArgs && compositorArgs[baseAction])
return { type: "compositor", base: baseAction, config: compositorArgs[baseAction] };
for (var key in DMS_ACTION_ARGS) { for (var key in DMS_ACTION_ARGS) {
if (action.startsWith(DMS_ACTION_ARGS[key].base)) if (action.startsWith(DMS_ACTION_ARGS[key].base))
@@ -429,7 +944,7 @@ function getActionArgConfig(action) {
return null; return null;
} }
function parseCompositorActionArgs(action) { function parseCompositorActionArgs(compositor, action) {
if (!action) if (!action)
return { base: "", args: {} }; return { base: "", args: {} };
@@ -437,44 +952,144 @@ function parseCompositorActionArgs(action) {
var base = parts[0]; var base = parts[0];
var args = {}; var args = {};
if (!ACTION_ARGS[base]) var compositorArgs = ACTION_ARGS[compositor];
if (!compositorArgs || !compositorArgs[base])
return { base: action, args: {} }; return { base: action, args: {} };
var argConfig = compositorArgs[base];
var argParts = parts.slice(1); var argParts = parts.slice(1);
switch (base) { switch (compositor) {
case "move-column-to-workspace": case "niri":
for (var i = 0; i < argParts.length; i++) { switch (base) {
if (argParts[i] === "focus=true" || argParts[i] === "focus=false") { case "move-column-to-workspace":
args.focus = argParts[i] === "focus=true"; for (var i = 0; i < argParts.length; i++) {
} else if (!args.index) { if (argParts[i] === "focus=true" || argParts[i] === "focus=false") {
args.index = argParts[i]; args.focus = argParts[i] === "focus=true";
} else if (!args.index) {
args.index = argParts[i];
}
}
break;
case "move-column-to-workspace-down":
case "move-column-to-workspace-up":
for (var k = 0; k < argParts.length; k++) {
if (argParts[k] === "focus=true" || argParts[k] === "focus=false")
args.focus = argParts[k] === "focus=true";
}
break;
default:
if (base.startsWith("screenshot")) {
for (var j = 0; j < argParts.length; j++) {
var kv = argParts[j].split("=");
if (kv.length === 2)
args[kv[0]] = kv[1] === "true";
}
} else if (argParts.length > 0) {
args.value = argParts.join(" ");
} }
} }
break; break;
case "move-column-to-workspace-down": case "mangowc":
case "move-column-to-workspace-up": if (argConfig.args && argConfig.args.length > 0 && argParts.length > 0) {
for (var k = 0; k < argParts.length; k++) { var paramStr = argParts.join(" ");
if (argParts[k] === "focus=true" || argParts[k] === "focus=false") var paramValues = paramStr.split(",");
args.focus = argParts[k] === "focus=true"; for (var m = 0; m < argConfig.args.length && m < paramValues.length; m++) {
args[argConfig.args[m].name] = paramValues[m];
}
}
break;
case "hyprland":
if (argConfig.args && argConfig.args.length > 0) {
switch (base) {
case "resizewindowpixel":
case "movewindowpixel":
var commaIdx = argParts.join(" ").indexOf(",");
if (commaIdx !== -1) {
var fullStr = argParts.join(" ");
args[argConfig.args[0].name] = fullStr.substring(0, commaIdx);
args[argConfig.args[1].name] = fullStr.substring(commaIdx + 1);
} else if (argParts.length > 0) {
args[argConfig.args[0].name] = argParts.join(" ");
}
break;
case "movetoworkspace":
case "movetoworkspacesilent":
case "tagwindow":
case "alterzorder":
if (argParts.length >= 2) {
args[argConfig.args[0].name] = argParts[0];
args[argConfig.args[1].name] = argParts.slice(1).join(" ");
} else if (argParts.length === 1) {
args[argConfig.args[0].name] = argParts[0];
}
break;
case "moveworkspacetomonitor":
case "swapactiveworkspaces":
case "renameworkspace":
case "fullscreenstate":
case "movecursor":
if (argParts.length >= 2) {
args[argConfig.args[0].name] = argParts[0];
args[argConfig.args[1].name] = argParts[1];
} else if (argParts.length === 1) {
args[argConfig.args[0].name] = argParts[0];
}
break;
case "setprop":
if (argParts.length >= 3) {
args.window = argParts[0];
args.property = argParts[1];
args.value = argParts.slice(2).join(" ");
} else if (argParts.length === 2) {
args.window = argParts[0];
args.property = argParts[1];
}
break;
case "sendshortcut":
if (argParts.length >= 3) {
args.mod = argParts[0];
args.key = argParts[1];
args.window = argParts.slice(2).join(" ");
} else if (argParts.length >= 2) {
args.mod = argParts[0];
args.key = argParts[1];
}
break;
case "sendkeystate":
if (argParts.length >= 4) {
args.mod = argParts[0];
args.key = argParts[1];
args.state = argParts[2];
args.window = argParts.slice(3).join(" ");
}
break;
case "signalwindow":
if (argParts.length >= 2) {
args.window = argParts[0];
args.signal = argParts[1];
}
break;
default:
if (argParts.length > 0) {
if (argConfig.args.length === 1) {
args[argConfig.args[0].name] = argParts.join(" ");
} else {
args.value = argParts.join(" ");
}
}
}
} }
break; break;
default: default:
if (base.startsWith("screenshot")) { if (argParts.length > 0)
for (var j = 0; j < argParts.length; j++) {
var kv = argParts[j].split("=");
if (kv.length === 2)
args[kv[0]] = kv[1] === "true";
}
} else if (argParts.length > 0) {
args.value = argParts.join(" "); args.value = argParts.join(" ");
}
} }
return { base: base, args: args }; return { base: base, args: args };
} }
function buildCompositorAction(base, args) { function buildCompositorAction(compositor, base, args) {
if (!base) if (!base)
return ""; return "";
@@ -483,29 +1098,126 @@ function buildCompositorAction(base, args) {
if (!args || Object.keys(args).length === 0) if (!args || Object.keys(args).length === 0)
return base; return base;
switch (base) { switch (compositor) {
case "move-column-to-workspace": case "niri":
if (args.index) switch (base) {
parts.push(args.index); case "move-column-to-workspace":
if (args.focus === false) if (args.index)
parts.push("focus=false"); parts.push(args.index);
if (args.focus === false)
parts.push("focus=false");
break;
case "move-column-to-workspace-down":
case "move-column-to-workspace-up":
if (args.focus === false)
parts.push("focus=false");
break;
default:
switch (base) {
case "screenshot":
if (args["show-pointer"] === true)
parts.push("show-pointer=true");
else if (args["show-pointer"] === false)
parts.push("show-pointer=false");
break;
case "screenshot-screen":
if (args["show-pointer"] === true)
parts.push("show-pointer=true");
else if (args["show-pointer"] === false)
parts.push("show-pointer=false");
if (args["write-to-disk"] === true)
parts.push("write-to-disk=true");
break;
case "screenshot-window":
if (args["write-to-disk"] === true)
parts.push("write-to-disk=true");
break;
}
if (args.value) {
parts.push(args.value);
} else if (args.index) {
parts.push(args.index);
}
}
break; break;
case "move-column-to-workspace-down": case "mangowc":
case "move-column-to-workspace-up": var compositorArgs = ACTION_ARGS.mangowc;
if (args.focus === false) if (compositorArgs && compositorArgs[base] && compositorArgs[base].args) {
parts.push("focus=false"); var argConfig = compositorArgs[base].args;
break; var argValues = [];
default: for (var i = 0; i < argConfig.length; i++) {
if (base.startsWith("screenshot")) { var argDef = argConfig[i];
if (args["show-pointer"] === true) var val = args[argDef.name];
parts.push("show-pointer=true"); if (val === undefined || val === "")
if (args["write-to-disk"] === true) val = argDef.default || "";
parts.push("write-to-disk=true"); if (val === "" && argValues.length === 0)
continue;
argValues.push(val);
}
if (argValues.length > 0)
parts.push(argValues.join(","));
} else if (args.value) { } else if (args.value) {
parts.push(args.value); parts.push(args.value);
} else if (args.index) {
parts.push(args.index);
} }
break;
case "hyprland":
var hyprArgs = ACTION_ARGS.hyprland;
if (hyprArgs && hyprArgs[base] && hyprArgs[base].args) {
var hyprConfig = hyprArgs[base].args;
switch (base) {
case "resizewindowpixel":
case "movewindowpixel":
if (args[hyprConfig[0].name])
parts.push(args[hyprConfig[0].name]);
if (args[hyprConfig[1].name])
parts[parts.length - 1] += "," + args[hyprConfig[1].name];
break;
case "setprop":
if (args.window)
parts.push(args.window);
if (args.property)
parts.push(args.property);
if (args.value)
parts.push(args.value);
break;
case "sendshortcut":
if (args.mod)
parts.push(args.mod);
if (args.key)
parts.push(args.key);
if (args.window)
parts.push(args.window);
break;
case "sendkeystate":
if (args.mod)
parts.push(args.mod);
if (args.key)
parts.push(args.key);
if (args.state)
parts.push(args.state);
if (args.window)
parts.push(args.window);
break;
case "signalwindow":
if (args.window)
parts.push(args.window);
if (args.signal)
parts.push(args.signal);
break;
default:
for (var j = 0; j < hyprConfig.length; j++) {
var hVal = args[hyprConfig[j].name];
if (hVal !== undefined && hVal !== "")
parts.push(hVal);
}
}
} else if (args.value) {
parts.push(args.value);
}
break;
default:
if (args.value)
parts.push(args.value);
} }
return parts.join(" "); return parts.join(" ");

View File

@@ -13,17 +13,16 @@ Singleton {
property var currentModalsByScreen: ({}) property var currentModalsByScreen: ({})
function openModal(modal) { function openModal(modal) {
if (!modal.allowStacking) {
closeAllModalsExcept(modal);
}
if (!modal.keepPopoutsOpen) {
PopoutManager.closeAllPopouts();
}
TrayMenuManager.closeAllMenus();
const screenName = modal.effectiveScreen?.name ?? "unknown"; const screenName = modal.effectiveScreen?.name ?? "unknown";
currentModalsByScreen[screenName] = modal; currentModalsByScreen[screenName] = modal;
modalChanged(); modalChanged();
Qt.callLater(() => {
if (!modal.allowStacking)
closeAllModalsExcept(modal);
if (!modal.keepPopoutsOpen)
PopoutManager.closeAllPopouts();
TrayMenuManager.closeAllMenus();
});
} }
function closeModal(modal) { function closeModal(modal) {

View File

@@ -45,20 +45,28 @@ Singleton {
Quickshell.execDetached(["cp", strip(from), strip(to)]); Quickshell.execDetached(["cp", strip(from), strip(to)]);
} }
function isSteamApp(appId: string): bool {
return appId && /^steam_app_\d+$/.test(appId);
}
function moddedAppId(appId: string): string { function moddedAppId(appId: string): string {
if (appId === "Spotify") const subs = SettingsData.appIdSubstitutions || [];
return "spotify"; for (let i = 0; i < subs.length; i++) {
if (appId === "beepertexts") const sub = subs[i];
return "beeper"; if (sub.type === "exact" && appId === sub.pattern) {
if (appId === "home assistant desktop") return sub.replacement;
return "homeassistant-desktop"; } else if (sub.type === "contains" && appId.includes(sub.pattern)) {
if (appId.includes("com.transmissionbt.transmission")) { return sub.replacement;
if (DesktopEntries.heuristicLookup("transmission-gtk")) } else if (sub.type === "regex") {
return "transmission-gtk"; const match = appId.match(new RegExp(sub.pattern));
if (DesktopEntries.heuristicLookup("transmission")) if (match) {
return "transmission"; return sub.replacement.replace(/\$(\d+)/g, (_, n) => match[n] || "");
return "transmission-gtk"; }
}
} }
const steamMatch = appId.match(/^steam_app_(\d+)$/);
if (steamMatch)
return `steam_icon_${steamMatch[1]}`;
return appId; return appId;
} }
@@ -68,8 +76,8 @@ Singleton {
} }
const moddedId = moddedAppId(appId); const moddedId = moddedAppId(appId);
if (moddedId.toLowerCase().includes("steam_app")) { if (moddedId !== appId) {
return ""; return Quickshell.iconPath(moddedId, true);
} }
return desktopEntry && desktopEntry.icon ? Quickshell.iconPath(desktopEntry.icon, true) : ""; return desktopEntry && desktopEntry.icon ? Quickshell.iconPath(desktopEntry.icon, true) : "";

View File

@@ -82,15 +82,19 @@ Singleton {
popoutOpening(); popoutOpening();
} }
let justClosedSamePopout = false; let movedFromOtherScreen = false;
for (const otherScreenName in currentPopoutsByScreen) { for (const otherScreenName in currentPopoutsByScreen) {
if (otherScreenName === screenName) if (otherScreenName === screenName)
continue; continue;
const otherPopout = currentPopoutsByScreen[otherScreenName]; const otherPopout = currentPopoutsByScreen[otherScreenName];
if (!otherPopout) if (!otherPopout)
continue; continue;
if (otherPopout === popout) { if (otherPopout === popout) {
justClosedSamePopout = true; movedFromOtherScreen = true;
currentPopoutsByScreen[otherScreenName] = null;
currentPopoutTriggers[otherScreenName] = null;
continue;
} }
if (otherPopout.dashVisible !== undefined) { if (otherPopout.dashVisible !== undefined) {
@@ -112,7 +116,7 @@ Singleton {
} }
} }
if (currentPopout === popout && popout.shouldBeVisible) { if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) { if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
if (popout.dashVisible !== undefined) { if (popout.dashVisible !== undefined) {
popout.dashVisible = false; popout.dashVisible = false;
@@ -139,6 +143,7 @@ Singleton {
popout.currentTabIndex = tabIndex; popout.currentTabIndex = tabIndex;
} }
currentPopoutTriggers[screenName] = triggerId; currentPopoutTriggers[screenName] = triggerId;
return;
} }
currentPopoutTriggers[screenName] = triggerId; currentPopoutTriggers[screenName] = triggerId;
@@ -153,16 +158,8 @@ Singleton {
ModalManager.closeAllModalsExcept(null); ModalManager.closeAllModalsExcept(null);
} }
if (justClosedSamePopout) { if (movedFromOtherScreen) {
Qt.callLater(() => { popout.open();
if (popout.dashVisible !== undefined) {
popout.dashVisible = true;
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = true;
} else {
popout.open();
}
});
} else { } else {
if (popout.dashVisible !== undefined) { if (popout.dashVisible !== undefined) {
popout.dashVisible = true; popout.dashVisible = true;

View File

@@ -1,5 +1,5 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior pragma ComponentBehavior: Bound
import QtCore import QtCore
import QtQuick import QtQuick
@@ -145,6 +145,7 @@ Singleton {
property bool controlCenterShowMicPercent: true property bool controlCenterShowMicPercent: true
property bool controlCenterShowBatteryIcon: false property bool controlCenterShowBatteryIcon: false
property bool controlCenterShowPrinterIcon: false property bool controlCenterShowPrinterIcon: false
property bool controlCenterShowScreenSharingIcon: true
property bool showPrivacyButton: true property bool showPrivacyButton: true
property bool privacyShowMicIcon: false property bool privacyShowMicIcon: false
property bool privacyShowCameraIcon: false property bool privacyShowCameraIcon: false
@@ -200,10 +201,16 @@ Singleton {
property bool showWorkspaceApps: false property bool showWorkspaceApps: false
property bool groupWorkspaceApps: true property bool groupWorkspaceApps: true
property int maxWorkspaceIcons: 3 property int maxWorkspaceIcons: 3
property bool workspacesPerMonitor: true property bool workspaceFollowFocus: false
property bool showOccupiedWorkspacesOnly: false property bool showOccupiedWorkspacesOnly: false
property bool reverseScrolling: false property bool reverseScrolling: false
property bool dwlShowAllTags: false property bool dwlShowAllTags: false
property string workspaceColorMode: "default"
property string workspaceUnfocusedColorMode: "default"
property string workspaceUrgentColorMode: "default"
property bool workspaceFocusedBorderEnabled: false
property string workspaceFocusedBorderColor: "primary"
property int workspaceFocusedBorderThickness: 2
property var workspaceNameIcons: ({}) property var workspaceNameIcons: ({})
property bool waveProgressEnabled: true property bool waveProgressEnabled: true
property bool scrollTitleEnabled: true property bool scrollTitleEnabled: true
@@ -215,6 +222,7 @@ Singleton {
property bool keyboardLayoutNameCompactMode: false property bool keyboardLayoutNameCompactMode: false
property bool runningAppsCurrentWorkspace: false property bool runningAppsCurrentWorkspace: false
property bool runningAppsGroupByApp: false property bool runningAppsGroupByApp: false
property var appIdSubstitutions: []
property string centeringMode: "index" property string centeringMode: "index"
property string clockDateFormat: "" property string clockDateFormat: ""
property string lockDateFormat: "" property string lockDateFormat: ""
@@ -318,9 +326,9 @@ Singleton {
property int batteryChargeLimit: 100 property int batteryChargeLimit: 100
property bool lockBeforeSuspend: false property bool lockBeforeSuspend: false
property bool loginctlLockIntegration: true property bool loginctlLockIntegration: true
property bool fadeToLockEnabled: false property bool fadeToLockEnabled: true
property int fadeToLockGracePeriod: 5 property int fadeToLockGracePeriod: 5
property bool fadeToDpmsEnabled: false property bool fadeToDpmsEnabled: true
property int fadeToDpmsGracePeriod: 5 property int fadeToDpmsGracePeriod: 5
property string launchPrefix: "" property string launchPrefix: ""
property var brightnessDevicePins: ({}) property var brightnessDevicePins: ({})
@@ -397,6 +405,7 @@ Singleton {
property int notificationTimeoutLow: 5000 property int notificationTimeoutLow: 5000
property int notificationTimeoutNormal: 5000 property int notificationTimeoutNormal: 5000
property int notificationTimeoutCritical: 0 property int notificationTimeoutCritical: 0
property bool notificationCompactMode: false
property int notificationPopupPosition: SettingsData.Position.Top property int notificationPopupPosition: SettingsData.Position.Top
property bool notificationHistoryEnabled: true property bool notificationHistoryEnabled: true
property int notificationHistoryMaxCount: 50 property int notificationHistoryMaxCount: 50
@@ -530,6 +539,7 @@ Singleton {
property var desktopWidgetPositions: ({}) property var desktopWidgetPositions: ({})
property var desktopWidgetGridSettings: ({}) property var desktopWidgetGridSettings: ({})
property var desktopWidgetInstances: [] property var desktopWidgetInstances: []
property var desktopWidgetGroups: []
function getDesktopWidgetGridSetting(screenKey, property, defaultValue) { function getDesktopWidgetGridSetting(screenKey, property, defaultValue) {
const val = desktopWidgetGridSettings?.[screenKey]?.[property]; const val = desktopWidgetGridSettings?.[screenKey]?.[property];
@@ -681,6 +691,38 @@ Singleton {
saveSettings(); saveSettings();
} }
function syncDesktopWidgetPositionToAllScreens(instanceId) {
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
const idx = instances.findIndex(inst => inst.id === instanceId);
if (idx === -1)
return;
const positions = instances[idx].positions || {};
const screenKeys = Object.keys(positions).filter(k => k !== "_synced");
if (screenKeys.length === 0)
return;
const sourceKey = screenKeys[0];
const sourcePos = positions[sourceKey];
if (!sourcePos)
return;
const screen = Array.from(Quickshell.screens.values()).find(s => getScreenDisplayName(s) === sourceKey);
if (!screen)
return;
const screenW = screen.width;
const screenH = screen.height;
const synced = {};
if (sourcePos.x !== undefined)
synced.x = sourcePos.x / screenW;
if (sourcePos.y !== undefined)
synced.y = sourcePos.y / screenH;
if (sourcePos.width !== undefined)
synced.width = sourcePos.width;
if (sourcePos.height !== undefined)
synced.height = sourcePos.height;
instances[idx].positions["_synced"] = synced;
desktopWidgetInstances = instances;
saveSettings();
}
function duplicateDesktopWidgetInstance(instanceId) { function duplicateDesktopWidgetInstance(instanceId) {
const source = getDesktopWidgetInstance(instanceId); const source = getDesktopWidgetInstance(instanceId);
if (!source) if (!source)
@@ -713,6 +755,110 @@ Singleton {
return (desktopWidgetInstances || []).filter(inst => inst.enabled); return (desktopWidgetInstances || []).filter(inst => inst.enabled);
} }
function moveDesktopWidgetInstance(instanceId, direction) {
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
const idx = instances.findIndex(inst => inst.id === instanceId);
if (idx === -1)
return false;
const targetIdx = direction === "up" ? idx - 1 : idx + 1;
if (targetIdx < 0 || targetIdx >= instances.length)
return false;
const temp = instances[idx];
instances[idx] = instances[targetIdx];
instances[targetIdx] = temp;
desktopWidgetInstances = instances;
saveSettings();
return true;
}
function reorderDesktopWidgetInstance(instanceId, newIndex) {
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
const idx = instances.findIndex(inst => inst.id === instanceId);
if (idx === -1 || newIndex < 0 || newIndex >= instances.length)
return false;
const [item] = instances.splice(idx, 1);
instances.splice(newIndex, 0, item);
desktopWidgetInstances = instances;
saveSettings();
return true;
}
function reorderDesktopWidgetInstanceInGroup(instanceId, groupId, newIndexInGroup) {
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
const groups = desktopWidgetGroups || [];
const groupMatches = inst => {
if (groupId === null)
return !inst.group || !groups.some(g => g.id === inst.group);
return inst.group === groupId;
};
const groupInstances = instances.filter(groupMatches);
const currentGroupIdx = groupInstances.findIndex(inst => inst.id === instanceId);
if (currentGroupIdx === -1 || currentGroupIdx === newIndexInGroup)
return false;
if (newIndexInGroup < 0 || newIndexInGroup >= groupInstances.length)
return false;
const globalIdx = instances.findIndex(inst => inst.id === instanceId);
if (globalIdx === -1)
return false;
const [item] = instances.splice(globalIdx, 1);
const targetInstance = groupInstances[newIndexInGroup];
let targetGlobalIdx = instances.findIndex(inst => inst.id === targetInstance.id);
if (newIndexInGroup > currentGroupIdx)
targetGlobalIdx++;
instances.splice(targetGlobalIdx, 0, item);
desktopWidgetInstances = instances;
saveSettings();
return true;
}
function createDesktopWidgetGroup(name) {
const id = "dwg_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9);
const group = {
id: id,
name: name,
collapsed: false
};
const groups = JSON.parse(JSON.stringify(desktopWidgetGroups || []));
groups.push(group);
desktopWidgetGroups = groups;
saveSettings();
return group;
}
function updateDesktopWidgetGroup(groupId, updates) {
const groups = JSON.parse(JSON.stringify(desktopWidgetGroups || []));
const idx = groups.findIndex(g => g.id === groupId);
if (idx === -1)
return;
Object.assign(groups[idx], updates);
desktopWidgetGroups = groups;
saveSettings();
}
function removeDesktopWidgetGroup(groupId) {
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
for (let i = 0; i < instances.length; i++) {
if (instances[i].group === groupId)
instances[i].group = null;
}
desktopWidgetInstances = instances;
const groups = (desktopWidgetGroups || []).filter(g => g.id !== groupId);
desktopWidgetGroups = groups;
saveSettings();
}
function getDesktopWidgetGroup(groupId) {
return (desktopWidgetGroups || []).find(g => g.id === groupId) || null;
}
function getDesktopWidgetInstancesByGroup(groupId) {
return (desktopWidgetInstances || []).filter(inst => inst.group === groupId);
}
function getUngroupedDesktopWidgetInstances() {
return (desktopWidgetInstances || []).filter(inst => !inst.group);
}
signal forceDankBarLayoutRefresh signal forceDankBarLayoutRefresh
signal forceDockLayoutRefresh signal forceDockLayoutRefresh
signal widgetDataChanged signal widgetDataChanged
@@ -1587,6 +1733,9 @@ Singleton {
updateCompositorCursor(); updateCompositorCursor();
} }
// This solution for xwayland cursor themes is from the xwls discussion:
// https://github.com/Supreeeme/xwayland-satellite/issues/104
// no idea if this matters on other compositors but we also set XCURSOR stuff in the launcher
function updateCompositorCursor() { function updateCompositorCursor() {
updateXResources(); updateXResources();
if (typeof CompositorService === "undefined") if (typeof CompositorService === "undefined")
@@ -1833,6 +1982,48 @@ Singleton {
return workspaceNameIcons[workspaceName] || null; return workspaceNameIcons[workspaceName] || null;
} }
function addAppIdSubstitution(pattern, replacement, type) {
var subs = JSON.parse(JSON.stringify(appIdSubstitutions));
subs.push({
pattern: pattern,
replacement: replacement,
type: type
});
appIdSubstitutions = subs;
saveSettings();
}
function updateAppIdSubstitution(index, pattern, replacement, type) {
var subs = JSON.parse(JSON.stringify(appIdSubstitutions));
if (index < 0 || index >= subs.length)
return;
subs[index] = {
pattern: pattern,
replacement: replacement,
type: type
};
appIdSubstitutions = subs;
saveSettings();
}
function removeAppIdSubstitution(index) {
var subs = JSON.parse(JSON.stringify(appIdSubstitutions));
if (index < 0 || index >= subs.length)
return;
subs.splice(index, 1);
appIdSubstitutions = subs;
saveSettings();
}
function getDefaultAppIdSubstitutions() {
return Spec.SPEC.appIdSubstitutions.def;
}
function resetAppIdSubstitutions() {
appIdSubstitutions = JSON.parse(JSON.stringify(Spec.SPEC.appIdSubstitutions.def));
saveSettings();
}
function getRegistryThemeVariant(themeId, defaultVariant) { function getRegistryThemeVariant(themeId, defaultVariant) {
var stored = registryThemeVariants[themeId]; var stored = registryThemeVariants[themeId];
if (typeof stored === "string") if (typeof stored === "string")

View File

@@ -546,7 +546,7 @@ Singleton {
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode) if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode)
SessionData.setLightMode(light); SessionData.setLightMode(light);
if (!isGreeterMode) { if (!isGreeterMode) {
// Skip with matugen becuase, our script runner will do it. // Skip with matugen because, our script runner will do it.
if (!matugenAvailable) { if (!matugenAvailable) {
PortalService.setLightMode(light); PortalService.setLightMode(light);
} }
@@ -904,7 +904,7 @@ Singleton {
if (typeof SettingsData !== "undefined") { if (typeof SettingsData !== "undefined") {
const skipTemplates = []; const skipTemplates = [];
if (!SettingsData.runDmsMatugenTemplates) { if (!SettingsData.runDmsMatugenTemplates) {
skipTemplates.push("gtk", "neovim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode"); skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode");
} else { } else {
if (!SettingsData.matugenTemplateGtk) if (!SettingsData.matugenTemplateGtk)
skipTemplates.push("gtk"); skipTemplates.push("gtk");

View File

@@ -1,5 +1,5 @@
.pragma library .pragma library
// This exists only beacause I haven't been able to get linkColor to work with MarkdownText // This exists only because I haven't been able to get linkColor to work with MarkdownText
// May not be necessary if that's possible tbh. // May not be necessary if that's possible tbh.
function markdownToHtml(text) { function markdownToHtml(text) {
if (!text) return ""; if (!text) return "";

View File

@@ -28,7 +28,8 @@ Singleton {
showMicIcon: false, showMicIcon: false,
showMicPercent: true, showMicPercent: true,
showBatteryIcon: false, showBatteryIcon: false,
showPrinterIcon: false showPrinterIcon: false,
showScreenSharingIcon: true
}; };
leftModel.append(dummy); leftModel.append(dummy);
centerModel.append(dummy); centerModel.append(dummy);
@@ -84,6 +85,8 @@ Singleton {
item.showBatteryIcon = order[i].showBatteryIcon; item.showBatteryIcon = order[i].showBatteryIcon;
if (isObj && order[i].showPrinterIcon !== undefined) if (isObj && order[i].showPrinterIcon !== undefined)
item.showPrinterIcon = order[i].showPrinterIcon; item.showPrinterIcon = order[i].showPrinterIcon;
if (isObj && order[i].showScreenSharingIcon !== undefined)
item.showScreenSharingIcon = order[i].showScreenSharingIcon;
model.append(item); model.append(item);
} }

View File

@@ -70,6 +70,7 @@ var SPEC = {
controlCenterShowMicPercent: { def: false }, controlCenterShowMicPercent: { def: false },
controlCenterShowBatteryIcon: { def: false }, controlCenterShowBatteryIcon: { def: false },
controlCenterShowPrinterIcon: { def: false }, controlCenterShowPrinterIcon: { def: false },
controlCenterShowScreenSharingIcon: { def: true },
showPrivacyButton: { def: true }, showPrivacyButton: { def: true },
privacyShowMicIcon: { def: false }, privacyShowMicIcon: { def: false },
@@ -94,10 +95,16 @@ var SPEC = {
showWorkspaceApps: { def: false }, showWorkspaceApps: { def: false },
maxWorkspaceIcons: { def: 3 }, maxWorkspaceIcons: { def: 3 },
groupWorkspaceApps: { def: true }, groupWorkspaceApps: { def: true },
workspacesPerMonitor: { def: true }, workspaceFollowFocus: { def: false },
showOccupiedWorkspacesOnly: { def: false }, showOccupiedWorkspacesOnly: { def: false },
reverseScrolling: { def: false }, reverseScrolling: { def: false },
dwlShowAllTags: { def: false }, dwlShowAllTags: { def: false },
workspaceColorMode: { def: "default" },
workspaceUnfocusedColorMode: { def: "default" },
workspaceUrgentColorMode: { def: "default" },
workspaceFocusedBorderEnabled: { def: false },
workspaceFocusedBorderColor: { def: "primary" },
workspaceFocusedBorderThickness: { def: 2 },
workspaceNameIcons: { def: {} }, workspaceNameIcons: { def: {} },
waveProgressEnabled: { def: true }, waveProgressEnabled: { def: true },
scrollTitleEnabled: { def: true }, scrollTitleEnabled: { def: true },
@@ -109,6 +116,13 @@ var SPEC = {
keyboardLayoutNameCompactMode: { def: false }, keyboardLayoutNameCompactMode: { def: false },
runningAppsCurrentWorkspace: { def: false }, runningAppsCurrentWorkspace: { def: false },
runningAppsGroupByApp: { def: false }, runningAppsGroupByApp: { def: false },
appIdSubstitutions: { def: [
{ pattern: "Spotify", replacement: "spotify", type: "exact" },
{ pattern: "beepertexts", replacement: "beeper", type: "exact" },
{ pattern: "home assistant desktop", replacement: "homeassistant-desktop", type: "exact" },
{ pattern: "com.transmissionbt.transmission", replacement: "transmission-gtk", type: "contains" },
{ pattern: "^steam_app_(\\d+)$", replacement: "steam_icon_$1", type: "regex" }
]},
centeringMode: { def: "index" }, centeringMode: { def: "index" },
clockDateFormat: { def: "" }, clockDateFormat: { def: "" },
lockDateFormat: { def: "" }, lockDateFormat: { def: "" },
@@ -177,9 +191,9 @@ var SPEC = {
batteryChargeLimit: { def: 100 }, batteryChargeLimit: { def: 100 },
lockBeforeSuspend: { def: false }, lockBeforeSuspend: { def: false },
loginctlLockIntegration: { def: true }, loginctlLockIntegration: { def: true },
fadeToLockEnabled: { def: false }, fadeToLockEnabled: { def: true },
fadeToLockGracePeriod: { def: 5 }, fadeToLockGracePeriod: { def: 5 },
fadeToDpmsEnabled: { def: false }, fadeToDpmsEnabled: { def: true },
fadeToDpmsGracePeriod: { def: 5 }, fadeToDpmsGracePeriod: { def: 5 },
launchPrefix: { def: "" }, launchPrefix: { def: "" },
brightnessDevicePins: { def: {} }, brightnessDevicePins: { def: {} },
@@ -255,6 +269,7 @@ var SPEC = {
notificationTimeoutLow: { def: 5000 }, notificationTimeoutLow: { def: 5000 },
notificationTimeoutNormal: { def: 5000 }, notificationTimeoutNormal: { def: 5000 },
notificationTimeoutCritical: { def: 0 }, notificationTimeoutCritical: { def: 0 },
notificationCompactMode: { def: false },
notificationPopupPosition: { def: 0 }, notificationPopupPosition: { def: 0 },
notificationHistoryEnabled: { def: true }, notificationHistoryEnabled: { def: true },
notificationHistoryMaxCount: { def: 50 }, notificationHistoryMaxCount: { def: 50 },
@@ -388,6 +403,8 @@ var SPEC = {
desktopWidgetInstances: { def: [] }, desktopWidgetInstances: { def: [] },
desktopWidgetGroups: { def: [] },
builtInPluginSettings: { def: {} } builtInPluginSettings: { def: {} }
}; };

View File

@@ -2,6 +2,7 @@ import QtQuick
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Modals import qs.Modals
import qs.Modals.Changelog
import qs.Modals.Clipboard import qs.Modals.Clipboard
import qs.Modals.Greeter import qs.Modals.Greeter
import qs.Modals.Settings import qs.Modals.Settings
@@ -202,6 +203,8 @@ Item {
Component.onCompleted: { Component.onCompleted: {
dockRecreateDebounce.start(); dockRecreateDebounce.start();
// Force PolkitService singleton to initialize
PolkitService.polkitAvailable;
} }
Connections { Connections {
@@ -314,19 +317,44 @@ Item {
} }
} }
WifiPasswordModal { LazyLoader {
id: wifiPasswordModal id: wifiPasswordModalLoader
active: false
Component.onCompleted: { Component.onCompleted: {
PopoutService.wifiPasswordModal = wifiPasswordModal; PopoutService.wifiPasswordModalLoader = wifiPasswordModalLoader;
}
WifiPasswordModal {
id: wifiPasswordModalItem
Component.onCompleted: {
PopoutService.wifiPasswordModal = wifiPasswordModalItem;
}
} }
} }
PolkitAuthModal { LazyLoader {
id: polkitAuthModal id: polkitAuthModalLoader
active: false
Component.onCompleted: { PolkitAuthModal {
PopoutService.polkitAuthModal = polkitAuthModal; id: polkitAuthModal
Component.onCompleted: {
PopoutService.polkitAuthModal = polkitAuthModal;
}
}
}
Connections {
target: PolkitService.agent
enabled: PolkitService.polkitAvailable
function onAuthenticationRequestStarted() {
polkitAuthModalLoader.active = true;
if (polkitAuthModalLoader.item)
polkitAuthModalLoader.item.show();
} }
} }
@@ -348,17 +376,21 @@ Item {
const now = Date.now(); const now = Date.now();
const timeSinceLastPrompt = now - lastCredentialsTime; const timeSinceLastPrompt = now - lastCredentialsTime;
if (wifiPasswordModal.visible && timeSinceLastPrompt < 1000) { wifiPasswordModalLoader.active = true;
if (!wifiPasswordModalLoader.item)
return;
if (wifiPasswordModalLoader.item.visible && timeSinceLastPrompt < 1000) {
NetworkService.cancelCredentials(lastCredentialsToken); NetworkService.cancelCredentials(lastCredentialsToken);
lastCredentialsToken = token; lastCredentialsToken = token;
lastCredentialsTime = now; lastCredentialsTime = now;
wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo); wifiPasswordModalLoader.item.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo);
return; return;
} }
lastCredentialsToken = token; lastCredentialsToken = token;
lastCredentialsTime = now; lastCredentialsTime = now;
wifiPasswordModal.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo); wifiPasswordModalLoader.item.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo);
} }
} }
@@ -441,17 +473,15 @@ Item {
PopoutService.settingsModalLoader = settingsModalLoader; PopoutService.settingsModalLoader = settingsModalLoader;
} }
onActiveChanged: {
if (active && item) {
PopoutService.settingsModal = item;
PopoutService._onSettingsModalLoaded();
}
}
SettingsModal { SettingsModal {
id: settingsModal id: settingsModal
property bool wasShown: false property bool wasShown: false
Component.onCompleted: {
PopoutService.settingsModal = settingsModal;
PopoutService._onSettingsModalLoaded();
}
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
wasShown = true; wasShown = true;
@@ -836,9 +866,29 @@ Item {
function onGreeterRequested() { function onGreeterRequested() {
if (greeterLoader.active && greeterLoader.item) { if (greeterLoader.active && greeterLoader.item) {
greeterLoader.item.show(); greeterLoader.item.show();
} else { return;
greeterLoader.active = true;
} }
greeterLoader.active = true;
}
}
}
Loader {
id: changelogLoader
active: false
sourceComponent: ChangelogModal {
onChangelogDismissed: changelogLoader.active = false
Component.onCompleted: show()
}
Connections {
target: ChangelogService
function onChangelogRequested() {
if (changelogLoader.active && changelogLoader.item) {
changelogLoader.item.show();
return;
}
changelogLoader.active = true;
} }
} }
} }

View File

@@ -132,8 +132,11 @@ Item {
case "media": case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1; root.dankDashPopoutLoader.item.currentTabIndex = 1;
break; break;
case "wallpaper":
root.dankDashPopoutLoader.item.currentTabIndex = 2;
break;
case "weather": case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0; root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
break; break;
default: default:
root.dankDashPopoutLoader.item.currentTabIndex = 0; root.dankDashPopoutLoader.item.currentTabIndex = 0;
@@ -189,6 +192,13 @@ Item {
if (CompositorService.isNiri && NiriService.currentOutput) { if (CompositorService.isNiri && NiriService.currentOutput) {
return NiriService.currentOutput; return NiriService.currentOutput;
} }
if ((CompositorService.isSway || CompositorService.isScroll) && I3.workspaces?.values) {
const focusedWs = I3.workspaces.values.find(ws => ws.focused === true);
return focusedWs?.monitor?.name || "";
}
if (CompositorService.isDwl && DwlService.activeOutput) {
return DwlService.activeOutput;
}
return ""; return "";
} }
@@ -592,6 +602,39 @@ Item {
return barConfig.autoHide ? "BAR_MANUAL_HIDE_SUCCESS" : "BAR_AUTO_HIDE_SUCCESS"; return barConfig.autoHide ? "BAR_MANUAL_HIDE_SUCCESS" : "BAR_AUTO_HIDE_SUCCESS";
} }
function getPosition(selector: string, value: string): string {
const {
barConfig,
error
} = getBarConfig(selector, value);
if (error)
return error;
const positions = ["top", "bottom", "left", "right"];
return positions[barConfig.position] || "unknown";
}
function setPosition(selector: string, value: string, position: string): string {
const {
barConfig,
error
} = getBarConfig(selector, value);
if (error)
return error;
const positionMap = {
"top": SettingsData.Position.Top,
"bottom": SettingsData.Position.Bottom,
"left": SettingsData.Position.Left,
"right": SettingsData.Position.Right
};
const posValue = positionMap[position.toLowerCase()];
if (posValue === undefined)
return "BAR_INVALID_POSITION";
SettingsData.updateBarConfig(barConfig.id, {
position: posValue
});
return "BAR_POSITION_SET_SUCCESS";
}
target: "bar" target: "bar"
} }
@@ -757,11 +800,9 @@ Item {
const modal = PopoutService.settingsModal; const modal = PopoutService.settingsModal;
if (modal) { if (modal) {
if (type === "wallpaper") { if (type === "wallpaper") {
modal.wallpaperBrowser.allowStacking = false; modal.openWallpaperBrowser(false);
modal.wallpaperBrowser.open();
} else if (type === "profile") { } else if (type === "profile") {
modal.profileBrowser.allowStacking = false; modal.openProfileBrowser(false);
modal.profileBrowser.open();
} }
} else { } else {
PopoutService.openSettings(); PopoutService.openSettings();
@@ -1028,7 +1069,7 @@ Item {
const instances = SettingsData.desktopWidgetInstances || []; const instances = SettingsData.desktopWidgetInstances || [];
if (instances.length === 0) if (instances.length === 0)
return "No desktop widgets configured"; return "No desktop widgets configured";
return instances.map(i => `${i.id} [${i.widgetType}] ${i.name || i.widgetType}`).join("\n"); return instances.map(i => `${i.id} [${i.widgetType}] ${i.name || i.widgetType} ${i.enabled ? "[enabled]" : "[disabled]"}`).join("\n");
} }
function status(instanceId: string): string { function status(instanceId: string): string {
@@ -1039,9 +1080,115 @@ Item {
if (!instance) if (!instance)
return `DESKTOP_WIDGET_NOT_FOUND: ${instanceId}`; return `DESKTOP_WIDGET_NOT_FOUND: ${instanceId}`;
const enabled = instance.enabled ?? true;
const overlay = instance.config?.showOnOverlay ?? false; const overlay = instance.config?.showOnOverlay ?? false;
const overview = instance.config?.showOnOverview ?? false; const overview = instance.config?.showOnOverview ?? false;
return `overlay: ${overlay}, overview: ${overview}`; const clickThrough = instance.config?.clickThrough ?? false;
const syncPosition = instance.config?.syncPositionAcrossScreens ?? false;
return `enabled: ${enabled}, overlay: ${overlay}, overview: ${overview}, clickThrough: ${clickThrough}, syncPosition: ${syncPosition}`;
}
function enable(instanceId: string): string {
if (!instanceId)
return "ERROR: No instance ID specified";
const instance = SettingsData.getDesktopWidgetInstance(instanceId);
if (!instance)
return `DESKTOP_WIDGET_NOT_FOUND: ${instanceId}`;
SettingsData.updateDesktopWidgetInstance(instanceId, {
enabled: true
});
return `DESKTOP_WIDGET_ENABLED: ${instanceId}`;
}
function disable(instanceId: string): string {
if (!instanceId)
return "ERROR: No instance ID specified";
const instance = SettingsData.getDesktopWidgetInstance(instanceId);
if (!instance)
return `DESKTOP_WIDGET_NOT_FOUND: ${instanceId}`;
SettingsData.updateDesktopWidgetInstance(instanceId, {
enabled: false
});
return `DESKTOP_WIDGET_DISABLED: ${instanceId}`;
}
function toggleEnabled(instanceId: string): string {
if (!instanceId)
return "ERROR: No instance ID specified";
const instance = SettingsData.getDesktopWidgetInstance(instanceId);
if (!instance)
return `DESKTOP_WIDGET_NOT_FOUND: ${instanceId}`;
const currentValue = instance.enabled ?? true;
SettingsData.updateDesktopWidgetInstance(instanceId, {
enabled: !currentValue
});
return !currentValue ? `DESKTOP_WIDGET_ENABLED: ${instanceId}` : `DESKTOP_WIDGET_DISABLED: ${instanceId}`;
}
function toggleClickThrough(instanceId: string): string {
if (!instanceId)
return "ERROR: No instance ID specified";
const instance = SettingsData.getDesktopWidgetInstance(instanceId);
if (!instance)
return `DESKTOP_WIDGET_NOT_FOUND: ${instanceId}`;
const currentValue = instance.config?.clickThrough ?? false;
SettingsData.updateDesktopWidgetInstanceConfig(instanceId, {
clickThrough: !currentValue
});
return !currentValue ? `DESKTOP_WIDGET_CLICK_THROUGH_ENABLED: ${instanceId}` : `DESKTOP_WIDGET_CLICK_THROUGH_DISABLED: ${instanceId}`;
}
function setClickThrough(instanceId: string, enabled: string): string {
if (!instanceId)
return "ERROR: No instance ID specified";
const instance = SettingsData.getDesktopWidgetInstance(instanceId);
if (!instance)
return `DESKTOP_WIDGET_NOT_FOUND: ${instanceId}`;
const enabledBool = enabled === "true" || enabled === "1";
SettingsData.updateDesktopWidgetInstanceConfig(instanceId, {
clickThrough: enabledBool
});
return enabledBool ? `DESKTOP_WIDGET_CLICK_THROUGH_ENABLED: ${instanceId}` : `DESKTOP_WIDGET_CLICK_THROUGH_DISABLED: ${instanceId}`;
}
function toggleSyncPosition(instanceId: string): string {
if (!instanceId)
return "ERROR: No instance ID specified";
const instance = SettingsData.getDesktopWidgetInstance(instanceId);
if (!instance)
return `DESKTOP_WIDGET_NOT_FOUND: ${instanceId}`;
const currentValue = instance.config?.syncPositionAcrossScreens ?? false;
SettingsData.updateDesktopWidgetInstanceConfig(instanceId, {
syncPositionAcrossScreens: !currentValue
});
return !currentValue ? `DESKTOP_WIDGET_SYNC_POSITION_ENABLED: ${instanceId}` : `DESKTOP_WIDGET_SYNC_POSITION_DISABLED: ${instanceId}`;
}
function setSyncPosition(instanceId: string, enabled: string): string {
if (!instanceId)
return "ERROR: No instance ID specified";
const instance = SettingsData.getDesktopWidgetInstance(instanceId);
if (!instance)
return `DESKTOP_WIDGET_NOT_FOUND: ${instanceId}`;
const enabledBool = enabled === "true" || enabled === "1";
SettingsData.updateDesktopWidgetInstanceConfig(instanceId, {
syncPositionAcrossScreens: enabledBool
});
return enabledBool ? `DESKTOP_WIDGET_SYNC_POSITION_ENABLED: ${instanceId}` : `DESKTOP_WIDGET_SYNC_POSITION_DISABLED: ${instanceId}`;
} }
target: "desktopWidget" target: "desktopWidget"

View 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
}
}
}

View 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()
}
}

View 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/v1-2-release")
}
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;
}
}

View 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
}
}

View File

@@ -49,7 +49,7 @@ Item {
readonly property alias clickCatcher: clickCatcher readonly property alias clickCatcher: clickCatcher
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: useHyprlandFocusGrab || useBackground readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground
signal opened signal opened
signal dialogClosed signal dialogClosed
@@ -58,7 +58,6 @@ Item {
property bool animationsEnabled: true property bool animationsEnabled: true
function open() { function open() {
ModalManager.openModal(root);
closeTimer.stop(); closeTimer.stop();
const focusedScreen = CompositorService.getFocusedScreen(); const focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) { if (focusedScreen) {
@@ -66,6 +65,7 @@ Item {
if (!useSingleWindow) if (!useSingleWindow)
clickCatcher.screen = focusedScreen; clickCatcher.screen = focusedScreen;
} }
ModalManager.openModal(root);
shouldBeVisible = true; shouldBeVisible = true;
if (!useSingleWindow) if (!useSingleWindow)
clickCatcher.visible = true; clickCatcher.visible = true;
@@ -302,7 +302,7 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: root.useSingleWindow enabled: root.useSingleWindow && root.shouldBeVisible
hoverEnabled: false hoverEnabled: false
acceptedButtons: Qt.AllButtons acceptedButtons: Qt.AllButtons
onPressed: mouse.accepted = true onPressed: mouse.accepted = true

View File

@@ -8,6 +8,9 @@ import qs.Widgets
FocusScope { FocusScope {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation) property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
property string docsDir: StandardPaths.writableLocation(StandardPaths.DocumentsLocation) property string docsDir: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
property string musicDir: StandardPaths.writableLocation(StandardPaths.MusicLocation) property string musicDir: StandardPaths.writableLocation(StandardPaths.MusicLocation)
@@ -52,6 +55,12 @@ FocusScope {
signal fileSelected(string path) signal fileSelected(string path)
signal closeRequested signal closeRequested
function encodeFileUrl(path) {
if (!path)
return "";
return "file://" + path.split('/').map(s => encodeURIComponent(s)).join('/');
}
function initialize() { function initialize() {
loadSettings(); loadSettings();
currentPath = getLastPath(); currentPath = getLastPath();
@@ -188,7 +197,7 @@ FocusScope {
function handleSaveFile(filePath) { function handleSaveFile(filePath) {
var normalizedPath = filePath; var normalizedPath = filePath;
if (!normalizedPath.startsWith("file://")) { if (!normalizedPath.startsWith("file://")) {
normalizedPath = "file://" + filePath; normalizedPath = encodeFileUrl(filePath);
} }
var exists = false; var exists = false;
@@ -274,7 +283,7 @@ FocusScope {
nameFilters: fileExtensions nameFilters: fileExtensions
showFiles: true showFiles: true
showDirs: true showDirs: true
folder: currentPath ? "file://" + currentPath : "file://" + homeDir folder: encodeFileUrl(currentPath || homeDir)
sortField: { sortField: {
switch (sortBy) { switch (sortBy) {
case "name": case "name":

View File

@@ -21,67 +21,67 @@ StyledRect {
signal itemSelected(int index, string path, string name, bool isDir) signal itemSelected(int index, string path, string name, bool isDir)
function getFileExtension(fileName) { function getFileExtension(fileName) {
const parts = fileName.split('.') const parts = fileName.split('.');
if (parts.length > 1) { if (parts.length > 1) {
return parts[parts.length - 1].toLowerCase() return parts[parts.length - 1].toLowerCase();
} }
return "" return "";
} }
function determineFileType(fileName) { function determineFileType(fileName) {
const ext = getFileExtension(fileName) const ext = getFileExtension(fileName);
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"] const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"];
if (imageExts.includes(ext)) { if (imageExts.includes(ext)) {
return "image" return "image";
} }
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"] const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"];
if (videoExts.includes(ext)) { if (videoExts.includes(ext)) {
return "video" return "video";
} }
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"] const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"];
if (audioExts.includes(ext)) { if (audioExts.includes(ext)) {
return "audio" return "audio";
} }
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"] const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"];
if (codeExts.includes(ext)) { if (codeExts.includes(ext)) {
return "code" return "code";
} }
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"] const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"];
if (docExts.includes(ext)) { if (docExts.includes(ext)) {
return "document" return "document";
} }
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"] const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"];
if (archiveExts.includes(ext)) { if (archiveExts.includes(ext)) {
return "archive" return "archive";
} }
if (!ext || fileName.indexOf('.') === -1) { if (!ext || fileName.indexOf('.') === -1) {
return "binary" return "binary";
} }
return "file" return "file";
} }
function isImageFile(fileName) { function isImageFile(fileName) {
if (!fileName) { if (!fileName) {
return false return false;
} }
return determineFileType(fileName) === "image" return determineFileType(fileName) === "image";
} }
function getIconForFile(fileName) { function getIconForFile(fileName) {
const lowerName = fileName.toLowerCase() const lowerName = fileName.toLowerCase();
if (lowerName.startsWith("dockerfile")) { if (lowerName.startsWith("dockerfile")) {
return "docker" return "docker";
} }
const ext = fileName.split('.').pop() const ext = fileName.split('.').pop();
return ext || "" return ext || "";
} }
width: weMode ? 245 : iconSizes[iconSizeIndex] + 16 width: weMode ? 245 : iconSizes[iconSizeIndex] + 16
@@ -89,21 +89,21 @@ StyledRect {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (keyboardNavigationActive && delegateRoot.index === selectedIndex) if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
return Theme.surfacePressed return Theme.surfacePressed;
return mouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent" return mouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent";
} }
border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : "transparent" border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : "transparent"
border.width: (keyboardNavigationActive && delegateRoot.index === selectedIndex) ? 2 : 0 border.width: (keyboardNavigationActive && delegateRoot.index === selectedIndex) ? 2 : 0
Component.onCompleted: { Component.onCompleted: {
if (keyboardNavigationActive && delegateRoot.index === selectedIndex) if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir) itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
} }
onSelectedIndexChanged: { onSelectedIndexChanged: {
if (keyboardNavigationActive && selectedIndex === delegateRoot.index) if (keyboardNavigationActive && selectedIndex === delegateRoot.index)
itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir) itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
} }
Column { Column {
@@ -115,30 +115,31 @@ StyledRect {
height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8) height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8)
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
CachingImage { Image {
id: gridPreviewImage id: gridPreviewImage
anchors.fill: parent anchors.fill: parent
anchors.margins: 2 anchors.margins: 2
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga"] property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga"]
property int weExtIndex: 0 property int weExtIndex: 0
source: { property string imagePath: {
if (weMode && delegateRoot.fileIsDir) { if (weMode && delegateRoot.fileIsDir)
return "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex] return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
} return (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? delegateRoot.filePath : "";
return (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? ("file://" + delegateRoot.filePath) : ""
} }
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
onStatusChanged: { onStatusChanged: {
if (weMode && delegateRoot.fileIsDir && status === Image.Error) { if (weMode && delegateRoot.fileIsDir && status === Image.Error) {
if (weExtIndex < weExtensions.length - 1) { if (weExtIndex < weExtensions.length - 1) {
weExtIndex++ weExtIndex++;
source = "file://" + delegateRoot.filePath + "/preview" + weExtensions[weExtIndex]
} else { } else {
source = "" imagePath = "";
} }
} }
} }
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
maxCacheSize: weMode ? 225 : iconSizes[iconSizeIndex] sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex]
sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex]
asynchronous: true
visible: false visible: false
} }
@@ -198,7 +199,7 @@ StyledRect {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
itemClicked(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir) itemClicked(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
} }
} }
} }

View File

@@ -20,97 +20,97 @@ StyledRect {
signal itemSelected(int index, string path, string name, bool isDir) signal itemSelected(int index, string path, string name, bool isDir)
function getFileExtension(fileName) { function getFileExtension(fileName) {
const parts = fileName.split('.') const parts = fileName.split('.');
if (parts.length > 1) { if (parts.length > 1) {
return parts[parts.length - 1].toLowerCase() return parts[parts.length - 1].toLowerCase();
} }
return "" return "";
} }
function determineFileType(fileName) { function determineFileType(fileName) {
const ext = getFileExtension(fileName) const ext = getFileExtension(fileName);
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"] const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"];
if (imageExts.includes(ext)) { if (imageExts.includes(ext)) {
return "image" return "image";
} }
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"] const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"];
if (videoExts.includes(ext)) { if (videoExts.includes(ext)) {
return "video" return "video";
} }
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"] const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"];
if (audioExts.includes(ext)) { if (audioExts.includes(ext)) {
return "audio" return "audio";
} }
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"] const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"];
if (codeExts.includes(ext)) { if (codeExts.includes(ext)) {
return "code" return "code";
} }
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"] const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"];
if (docExts.includes(ext)) { if (docExts.includes(ext)) {
return "document" return "document";
} }
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"] const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"];
if (archiveExts.includes(ext)) { if (archiveExts.includes(ext)) {
return "archive" return "archive";
} }
if (!ext || fileName.indexOf('.') === -1) { if (!ext || fileName.indexOf('.') === -1) {
return "binary" return "binary";
} }
return "file" return "file";
} }
function isImageFile(fileName) { function isImageFile(fileName) {
if (!fileName) { if (!fileName) {
return false return false;
} }
return determineFileType(fileName) === "image" return determineFileType(fileName) === "image";
} }
function getIconForFile(fileName) { function getIconForFile(fileName) {
const lowerName = fileName.toLowerCase() const lowerName = fileName.toLowerCase();
if (lowerName.startsWith("dockerfile")) { if (lowerName.startsWith("dockerfile")) {
return "docker" return "docker";
} }
const ext = fileName.split('.').pop() const ext = fileName.split('.').pop();
return ext || "" return ext || "";
} }
function formatFileSize(size) { function formatFileSize(size) {
if (size < 1024) if (size < 1024)
return size + " B" return size + " B";
if (size < 1024 * 1024) if (size < 1024 * 1024)
return (size / 1024).toFixed(1) + " KB" return (size / 1024).toFixed(1) + " KB";
if (size < 1024 * 1024 * 1024) if (size < 1024 * 1024 * 1024)
return (size / (1024 * 1024)).toFixed(1) + " MB" return (size / (1024 * 1024)).toFixed(1) + " MB";
return (size / (1024 * 1024 * 1024)).toFixed(1) + " GB" return (size / (1024 * 1024 * 1024)).toFixed(1) + " GB";
} }
height: 44 height: 44
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex) if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex)
return Theme.surfacePressed return Theme.surfacePressed;
return listMouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent" return listMouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent";
} }
border.color: keyboardNavigationActive && listDelegateRoot.index === selectedIndex ? Theme.primary : "transparent" border.color: keyboardNavigationActive && listDelegateRoot.index === selectedIndex ? Theme.primary : "transparent"
border.width: (keyboardNavigationActive && listDelegateRoot.index === selectedIndex) ? 2 : 0 border.width: (keyboardNavigationActive && listDelegateRoot.index === selectedIndex) ? 2 : 0
Component.onCompleted: { Component.onCompleted: {
if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex) if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex)
itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir) itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
} }
onSelectedIndexChanged: { onSelectedIndexChanged: {
if (keyboardNavigationActive && selectedIndex === listDelegateRoot.index) if (keyboardNavigationActive && selectedIndex === listDelegateRoot.index)
itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir) itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
} }
Row { Row {
@@ -124,12 +124,15 @@ StyledRect {
height: 28 height: 28
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
CachingImage { Image {
id: listPreviewImage id: listPreviewImage
anchors.fill: parent anchors.fill: parent
source: (!listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName)) ? ("file://" + listDelegateRoot.filePath) : "" property string imagePath: (!listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName)) ? listDelegateRoot.filePath : ""
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
maxCacheSize: 32 sourceSize.width: 32
sourceSize.height: 32
asynchronous: true
visible: false visible: false
} }
@@ -203,7 +206,7 @@ StyledRect {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
itemClicked(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir) itemClicked(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
} }
} }
} }

View File

@@ -45,8 +45,12 @@ FloatingWindow {
parentModal.shouldHaveFocus = false; parentModal.shouldHaveFocus = false;
parentModal.allowFocusOverride = true; parentModal.allowFocusOverride = true;
} }
content.reset(); Qt.callLater(() => {
Qt.callLater(() => content.forceActiveFocus()); if (content) {
content.reset();
content.forceActiveFocus();
}
});
} else { } else {
if (parentModal && "allowFocusOverride" in parentModal) { if (parentModal && "allowFocusOverride" in parentModal) {
parentModal.allowFocusOverride = false; parentModal.allowFocusOverride = false;
@@ -56,27 +60,35 @@ FloatingWindow {
} }
} }
FileBrowserContent { Loader {
id: content id: contentLoader
anchors.fill: parent anchors.fill: parent
focus: true active: fileBrowserModal.visible
closeOnEscape: false sourceComponent: FileBrowserContent {
windowControls: windowControls id: content
anchors.fill: parent
focus: true
closeOnEscape: false
windowControls: fileBrowserModal.windowControlsRef
browserTitle: fileBrowserModal.browserTitle browserTitle: fileBrowserModal.browserTitle
browserIcon: fileBrowserModal.browserIcon browserIcon: fileBrowserModal.browserIcon
browserType: fileBrowserModal.browserType browserType: fileBrowserModal.browserType
fileExtensions: fileBrowserModal.fileExtensions fileExtensions: fileBrowserModal.fileExtensions
showHiddenFiles: fileBrowserModal.showHiddenFiles showHiddenFiles: fileBrowserModal.showHiddenFiles
saveMode: fileBrowserModal.saveMode saveMode: fileBrowserModal.saveMode
defaultFileName: fileBrowserModal.defaultFileName defaultFileName: fileBrowserModal.defaultFileName
Component.onCompleted: initialize() Component.onCompleted: initialize()
onFileSelected: path => fileBrowserModal.fileSelected(path) onFileSelected: path => fileBrowserModal.fileSelected(path)
onCloseRequested: fileBrowserModal.close() onCloseRequested: fileBrowserModal.close()
}
} }
property alias content: contentLoader.item
property alias windowControlsRef: windowControls
FloatingWindowControls { FloatingWindowControls {
id: windowControls id: windowControls
targetWindow: fileBrowserModal targetWindow: fileBrowserModal

View File

@@ -33,8 +33,12 @@ DankModal {
if (parentPopout) { if (parentPopout) {
parentPopout.customKeyboardFocus = WlrKeyboardFocus.None; parentPopout.customKeyboardFocus = WlrKeyboardFocus.None;
} }
content.reset(); Qt.callLater(() => {
Qt.callLater(() => content.forceActiveFocus()); if (contentLoader.item) {
contentLoader.item.reset();
contentLoader.item.forceActiveFocus();
}
});
} }
onDialogClosed: { onDialogClosed: {
@@ -43,8 +47,7 @@ DankModal {
} }
} }
directContent: FileBrowserContent { content: FileBrowserContent {
id: content
focus: true focus: true
browserTitle: fileBrowserSurfaceModal.browserTitle browserTitle: fileBrowserSurfaceModal.browserTitle

View File

@@ -9,12 +9,21 @@ Rectangle {
property string title: "" property string title: ""
property string description: "" property string description: ""
signal clicked
readonly property real iconContainerSize: Math.round(Theme.iconSize * 1.5) readonly property real iconContainerSize: Math.round(Theme.iconSize * 1.5)
height: Math.round(Theme.fontSizeMedium * 6.4) height: Math.round(Theme.fontSizeMedium * 6.4)
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh color: Theme.surfaceContainerHigh
Rectangle {
anchors.fill: parent
radius: parent.radius
color: Theme.primary
opacity: mouseArea.containsMouse ? 0.12 : 0
}
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingS
@@ -54,4 +63,12 @@ Rectangle {
} }
} }
} }
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
} }

View File

@@ -1,6 +1,8 @@
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
import Quickshell
import qs.Common import qs.Common
import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
@@ -87,6 +89,7 @@ Item {
iconName: "auto_awesome" iconName: "auto_awesome"
title: I18n.tr("Dynamic Theming", "greeter feature card title") title: I18n.tr("Dynamic Theming", "greeter feature card title")
description: I18n.tr("Colors from wallpaper", "greeter feature card description") description: I18n.tr("Colors from wallpaper", "greeter feature card description")
onClicked: PopoutService.openSettingsWithTab("theme")
} }
GreeterFeatureCard { GreeterFeatureCard {
@@ -94,6 +97,7 @@ Item {
iconName: "format_paint" iconName: "format_paint"
title: I18n.tr("App Theming", "greeter feature card title") title: I18n.tr("App Theming", "greeter feature card title")
description: I18n.tr("GTK, Qt, IDEs, more", "greeter feature card description") description: I18n.tr("GTK, Qt, IDEs, more", "greeter feature card description")
onClicked: PopoutService.openSettingsWithTab("theme")
} }
GreeterFeatureCard { GreeterFeatureCard {
@@ -101,6 +105,7 @@ Item {
iconName: "download" iconName: "download"
title: I18n.tr("Theme Registry", "greeter feature card title") title: I18n.tr("Theme Registry", "greeter feature card title")
description: I18n.tr("Community themes", "greeter feature card description") description: I18n.tr("Community themes", "greeter feature card description")
onClicked: PopoutService.openSettingsWithTab("theme")
} }
GreeterFeatureCard { GreeterFeatureCard {
@@ -108,6 +113,7 @@ Item {
iconName: "view_carousel" iconName: "view_carousel"
title: I18n.tr("DankBar", "greeter feature card title") title: I18n.tr("DankBar", "greeter feature card title")
description: I18n.tr("Modular widget bar", "greeter feature card description") description: I18n.tr("Modular widget bar", "greeter feature card description")
onClicked: PopoutService.openSettingsWithTab("dankbar_settings")
} }
GreeterFeatureCard { GreeterFeatureCard {
@@ -115,6 +121,7 @@ Item {
iconName: "extension" iconName: "extension"
title: I18n.tr("Plugins", "greeter feature card title") title: I18n.tr("Plugins", "greeter feature card title")
description: I18n.tr("Extensible architecture", "greeter feature card description") description: I18n.tr("Extensible architecture", "greeter feature card description")
onClicked: PopoutService.openSettingsWithTab("plugins")
} }
GreeterFeatureCard { GreeterFeatureCard {
@@ -122,6 +129,10 @@ Item {
iconName: "layers" iconName: "layers"
title: I18n.tr("Multi-Monitor", "greeter feature card title") title: I18n.tr("Multi-Monitor", "greeter feature card title")
description: I18n.tr("Per-screen config", "greeter feature card description") description: I18n.tr("Per-screen config", "greeter feature card description")
onClicked: {
const hasDisplayConfig = CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl;
PopoutService.openSettingsWithTab(hasDisplayConfig ? "display_config" : "display_widgets");
}
} }
GreeterFeatureCard { GreeterFeatureCard {
@@ -129,6 +140,7 @@ Item {
iconName: "nightlight" iconName: "nightlight"
title: I18n.tr("Display Control", "greeter feature card title") title: I18n.tr("Display Control", "greeter feature card title")
description: I18n.tr("Night mode & gamma", "greeter feature card description") description: I18n.tr("Night mode & gamma", "greeter feature card description")
onClicked: PopoutService.openSettingsWithTab("display_gamma")
} }
GreeterFeatureCard { GreeterFeatureCard {
@@ -136,13 +148,16 @@ Item {
iconName: "tune" iconName: "tune"
title: I18n.tr("Control Center", "greeter feature card title") title: I18n.tr("Control Center", "greeter feature card title")
description: I18n.tr("Quick system toggles", "greeter feature card description") description: I18n.tr("Quick system toggles", "greeter feature card description")
// This is doing an IPC since its just easier and lazier to access the bar ref
onClicked: Quickshell.execDetached(["dms", "ipc", "call", "control-center", "open"])
} }
GreeterFeatureCard { GreeterFeatureCard {
width: (parent.width - Theme.spacingS * 2) / 3 width: (parent.width - Theme.spacingS * 2) / 3
iconName: "density_small" iconName: "lock"
title: I18n.tr("System Tray", "greeter feature card title") title: I18n.tr("Lock Screen", "greeter feature card title")
description: I18n.tr("Background app icons", "greeter feature card description") description: I18n.tr("Security & privacy", "greeter feature card description")
onClicked: PopoutService.openSettingsWithTab("lock_screen")
} }
} }
} }

View File

@@ -11,7 +11,6 @@ FloatingWindow {
property var currentFlow: PolkitService.agent?.flow property var currentFlow: PolkitService.agent?.flow
property bool isLoading: false property bool isLoading: false
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
property int calculatedHeight: Math.max(240, headerRow.implicitHeight + mainColumn.implicitHeight + Theme.spacingM * 3)
function focusPasswordField() { function focusPasswordField() {
passwordField.forceActiveFocus(); passwordField.forceActiveFocus();
@@ -37,15 +36,19 @@ FloatingWindow {
} }
function cancelAuth() { function cancelAuth() {
if (!currentFlow || isLoading) if (isLoading)
return; return;
currentFlow.cancelAuthenticationRequest(); if (currentFlow) {
currentFlow.cancelAuthenticationRequest();
return;
}
hide();
} }
objectName: "polkitAuthModal" objectName: "polkitAuthModal"
title: I18n.tr("Authentication") title: I18n.tr("Authentication")
minimumSize: Qt.size(420, calculatedHeight) minimumSize: Qt.size(460, 220)
maximumSize: Qt.size(420, calculatedHeight) maximumSize: Qt.size(460, 220)
color: Theme.surfaceContainer color: Theme.surfaceContainer
visible: false visible: false
@@ -108,29 +111,24 @@ FloatingWindow {
event.accepted = true; event.accepted = true;
} }
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 { Item {
id: headerRow id: headerSection
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.leftMargin: Theme.spacingM anchors.margins: Theme.spacingM
anchors.rightMargin: Theme.spacingM height: Math.max(titleColumn.implicitHeight, windowButtonRow.implicitHeight)
anchors.topMargin: Theme.spacingM
height: Math.max(titleColumn.height, buttonRow.height) MouseArea {
anchors.fill: parent
onPressed: windowControls.tryStartMove()
onDoubleClicked: windowControls.tryToggleMaximize()
}
Column { Column {
id: titleColumn id: titleColumn
anchors.left: parent.left anchors.left: parent.left
anchors.right: buttonRow.left anchors.right: windowButtonRow.left
anchors.rightMargin: Theme.spacingM anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingXS spacing: Theme.spacingXS
@@ -141,33 +139,34 @@ FloatingWindow {
font.weight: Font.Medium font.weight: Font.Medium
} }
Column { StyledText {
text: currentFlow?.message ?? ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width width: parent.width
spacing: Theme.spacingXS wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
visible: text !== ""
}
StyledText { StyledText {
text: currentFlow?.message ?? "" text: currentFlow?.supplementaryMessage ?? ""
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium color: (currentFlow?.supplementaryIsError ?? false) ? Theme.error : Theme.surfaceTextMedium
width: parent.width width: parent.width
wrapMode: Text.Wrap wrapMode: Text.Wrap
} maximumLineCount: 2
elide: Text.ElideRight
StyledText { opacity: (currentFlow?.supplementaryIsError ?? false) ? 1 : 0.8
visible: (currentFlow?.supplementaryMessage ?? "") !== "" visible: text !== ""
text: currentFlow?.supplementaryMessage ?? ""
font.pixelSize: Theme.fontSizeSmall
color: (currentFlow?.supplementaryIsError ?? false) ? Theme.error : Theme.surfaceTextMedium
width: parent.width
wrapMode: Text.Wrap
opacity: (currentFlow?.supplementaryIsError ?? false) ? 1 : 0.8
}
} }
} }
Row { Row {
id: buttonRow id: windowButtonRow
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankActionButton { DankActionButton {
@@ -190,21 +189,19 @@ FloatingWindow {
} }
Column { Column {
id: mainColumn id: bottomSection
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.leftMargin: Theme.spacingM anchors.margins: Theme.spacingM
anchors.rightMargin: Theme.spacingM spacing: Theme.spacingS
anchors.bottomMargin: Theme.spacingM
spacing: Theme.spacingM
StyledText { StyledText {
text: currentFlow?.inputPrompt ?? "" text: currentFlow?.inputPrompt ?? ""
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
width: parent.width width: parent.width
visible: (currentFlow?.inputPrompt ?? "") !== "" visible: text !== ""
} }
Rectangle { Rectangle {
@@ -229,7 +226,8 @@ FloatingWindow {
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText textColor: Theme.surfaceText
text: passwordInput text: passwordInput
echoMode: (currentFlow?.responseVisible ?? false) ? TextInput.Normal : TextInput.Password showPasswordToggle: !(currentFlow?.responseVisible ?? false)
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
placeholderText: "" placeholderText: ""
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: !isLoading enabled: !isLoading
@@ -238,38 +236,17 @@ FloatingWindow {
} }
} }
Item { StyledText {
text: I18n.tr("Authentication failed, please try again")
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
width: parent.width width: parent.width
height: (currentFlow?.failed ?? false) ? failedText.implicitHeight : 0 visible: currentFlow?.failed ?? false
visible: height > 0
StyledText {
id: failedText
text: I18n.tr("Authentication failed, please try again")
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
width: parent.width
opacity: (currentFlow?.failed ?? false) ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
Item { Item {
width: parent.width width: parent.width
height: 40 height: 36
Row { Row {
anchors.right: parent.right anchors.right: parent.right

View File

@@ -74,9 +74,7 @@ Rectangle {
if (root.parentModal) { if (root.parentModal) {
root.parentModal.allowFocusOverride = true; root.parentModal.allowFocusOverride = true;
root.parentModal.shouldHaveFocus = false; root.parentModal.shouldHaveFocus = false;
if (root.parentModal.profileBrowser) { root.parentModal.openProfileBrowser();
root.parentModal.profileBrowser.open();
}
} }
} }
} }
@@ -130,6 +128,7 @@ Rectangle {
color: Theme.surfaceText color: Theme.surfaceText
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
horizontalAlignment: Text.AlignLeft
} }
StyledText { StyledText {
@@ -138,6 +137,7 @@ Rectangle {
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
horizontalAlignment: Text.AlignLeft
} }
} }
} }

View File

@@ -8,8 +8,26 @@ import qs.Widgets
FloatingWindow { FloatingWindow {
id: settingsModal id: settingsModal
property alias profileBrowser: profileBrowser property var profileBrowser: profileBrowserLoader.item
property alias wallpaperBrowser: wallpaperBrowser property var wallpaperBrowser: wallpaperBrowserLoader.item
function openProfileBrowser(allowStacking) {
profileBrowserLoader.active = true;
if (!profileBrowserLoader.item)
return;
if (allowStacking !== undefined)
profileBrowserLoader.item.allowStacking = allowStacking;
profileBrowserLoader.item.open();
}
function openWallpaperBrowser(allowStacking) {
wallpaperBrowserLoader.active = true;
if (!wallpaperBrowserLoader.item)
return;
if (allowStacking !== undefined)
wallpaperBrowserLoader.item.allowStacking = allowStacking;
wallpaperBrowserLoader.item.open();
}
property alias sidebar: sidebar property alias sidebar: sidebar
property int currentTabIndex: 0 property int currentTabIndex: 0
property bool shouldHaveFocus: visible property bool shouldHaveFocus: visible
@@ -34,15 +52,19 @@ FloatingWindow {
} }
function showWithTab(tabIndex: int) { function showWithTab(tabIndex: int) {
if (tabIndex >= 0) if (tabIndex >= 0) {
currentTabIndex = tabIndex; currentTabIndex = tabIndex;
sidebar.autoExpandForTab(tabIndex);
}
visible = true; visible = true;
} }
function showWithTabName(tabName: string) { function showWithTabName(tabName: string) {
var idx = sidebar.resolveTabIndex(tabName); var idx = sidebar.resolveTabIndex(tabName);
if (idx >= 0) if (idx >= 0) {
currentTabIndex = idx; currentTabIndex = idx;
sidebar.autoExpandForTab(idx);
}
visible = true; visible = true;
} }
@@ -92,41 +114,51 @@ FloatingWindow {
} }
} }
FileBrowserModal { LazyLoader {
id: profileBrowser id: profileBrowserLoader
active: false
allowStacking: true FileBrowserModal {
parentModal: settingsModal id: profileBrowserItem
browserTitle: I18n.tr("Select Profile Image", "profile image file browser title")
browserIcon: "person" allowStacking: true
browserType: "profile" parentModal: settingsModal
showHiddenFiles: true browserTitle: I18n.tr("Select Profile Image", "profile image file browser title")
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] browserIcon: "person"
onFileSelected: path => { browserType: "profile"
PortalService.setProfileImage(path); showHiddenFiles: true
close(); fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
} onFileSelected: path => {
onDialogClosed: () => { PortalService.setProfileImage(path);
allowStacking = true; close();
}
onDialogClosed: () => {
allowStacking = true;
}
} }
} }
FileBrowserModal { LazyLoader {
id: wallpaperBrowser id: wallpaperBrowserLoader
active: false
allowStacking: true FileBrowserModal {
parentModal: settingsModal id: wallpaperBrowserItem
browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title")
browserIcon: "wallpaper" allowStacking: true
browserType: "wallpaper" parentModal: settingsModal
showHiddenFiles: true browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title")
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] browserIcon: "wallpaper"
onFileSelected: path => { browserType: "wallpaper"
SessionData.setWallpaper(path); showHiddenFiles: true
close(); fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
} onFileSelected: path => {
onDialogClosed: () => { SessionData.setWallpaper(path);
allowStacking = true; close();
}
onDialogClosed: () => {
allowStacking = true;
}
} }
} }
@@ -315,8 +347,8 @@ FloatingWindow {
visible: settingsModal.isCompactMode ? settingsModal.menuVisible : true visible: settingsModal.isCompactMode ? settingsModal.menuVisible : true
parentModal: settingsModal parentModal: settingsModal
currentIndex: settingsModal.currentTabIndex currentIndex: settingsModal.currentTabIndex
onCurrentIndexChanged: { onTabChangeRequested: tabIndex => {
settingsModal.currentTabIndex = currentIndex; settingsModal.currentTabIndex = tabIndex;
if (settingsModal.isCompactMode) { if (settingsModal.isCompactMode) {
settingsModal.enableAnimations = true; settingsModal.enableAnimations = true;
settingsModal.menuVisible = false; settingsModal.menuVisible = false;

View File

@@ -15,6 +15,8 @@ Rectangle {
property int currentIndex: 0 property int currentIndex: 0
property var parentModal: null property var parentModal: null
signal tabChangeRequested(int tabIndex)
property var expandedCategories: ({}) property var expandedCategories: ({})
property var autoExpandedCategories: ({}) property var autoExpandedCategories: ({})
property bool searchActive: searchField.text.length > 0 property bool searchActive: searchField.text.length > 0
@@ -55,8 +57,9 @@ Rectangle {
if (keyboardHighlightIndex < 0) if (keyboardHighlightIndex < 0)
return; return;
var oldIndex = currentIndex; var oldIndex = currentIndex;
currentIndex = keyboardHighlightIndex; var newIndex = keyboardHighlightIndex;
autoCollapseIfNeeded(oldIndex, currentIndex); tabChangeRequested(newIndex);
autoCollapseIfNeeded(oldIndex, newIndex);
keyboardHighlightIndex = -1; keyboardHighlightIndex = -1;
Qt.callLater(searchField.forceActiveFocus); Qt.callLater(searchField.forceActiveFocus);
} }
@@ -398,28 +401,32 @@ Rectangle {
var flatItems = getFlatNavigableItems(); var flatItems = getFlatNavigableItems();
var currentPos = flatItems.findIndex(item => item.tabIndex === currentIndex); var currentPos = flatItems.findIndex(item => item.tabIndex === currentIndex);
var oldIndex = currentIndex; var oldIndex = currentIndex;
var newIndex;
if (currentPos === -1) { if (currentPos === -1) {
currentIndex = flatItems[0]?.tabIndex ?? 0; newIndex = flatItems[0]?.tabIndex ?? 0;
} else { } else {
var nextPos = (currentPos + 1) % flatItems.length; var nextPos = (currentPos + 1) % flatItems.length;
currentIndex = flatItems[nextPos].tabIndex; newIndex = flatItems[nextPos].tabIndex;
} }
autoCollapseIfNeeded(oldIndex, currentIndex); tabChangeRequested(newIndex);
autoExpandForTab(currentIndex); autoCollapseIfNeeded(oldIndex, newIndex);
autoExpandForTab(newIndex);
} }
function navigatePrevious() { function navigatePrevious() {
var flatItems = getFlatNavigableItems(); var flatItems = getFlatNavigableItems();
var currentPos = flatItems.findIndex(item => item.tabIndex === currentIndex); var currentPos = flatItems.findIndex(item => item.tabIndex === currentIndex);
var oldIndex = currentIndex; var oldIndex = currentIndex;
var newIndex;
if (currentPos === -1) { if (currentPos === -1) {
currentIndex = flatItems[0]?.tabIndex ?? 0; newIndex = flatItems[0]?.tabIndex ?? 0;
} else { } else {
var prevPos = (currentPos - 1 + flatItems.length) % flatItems.length; var prevPos = (currentPos - 1 + flatItems.length) % flatItems.length;
currentIndex = flatItems[prevPos].tabIndex; newIndex = flatItems[prevPos].tabIndex;
} }
autoCollapseIfNeeded(oldIndex, currentIndex); tabChangeRequested(newIndex);
autoExpandForTab(currentIndex); autoCollapseIfNeeded(oldIndex, newIndex);
autoExpandForTab(newIndex);
} }
function getFlatNavigableItems() { function getFlatNavigableItems() {
@@ -488,7 +495,7 @@ Rectangle {
SettingsSearchService.navigateToSection(result.section); SettingsSearchService.navigateToSection(result.section);
} }
var oldIndex = root.currentIndex; var oldIndex = root.currentIndex;
root.currentIndex = result.tabIndex; tabChangeRequested(result.tabIndex);
autoCollapseIfNeeded(oldIndex, result.tabIndex); autoCollapseIfNeeded(oldIndex, result.tabIndex);
autoExpandForTab(result.tabIndex); autoExpandForTab(result.tabIndex);
searchField.text = ""; searchField.text = "";
@@ -807,7 +814,7 @@ Rectangle {
if (categoryDelegate.modelData.children) { if (categoryDelegate.modelData.children) {
root.toggleCategory(categoryDelegate.modelData.id); root.toggleCategory(categoryDelegate.modelData.id);
} else if (categoryDelegate.modelData.tabIndex !== undefined) { } else if (categoryDelegate.modelData.tabIndex !== undefined) {
root.currentIndex = categoryDelegate.modelData.tabIndex; root.tabChangeRequested(categoryDelegate.modelData.tabIndex);
} }
Qt.callLater(searchField.forceActiveFocus); Qt.callLater(searchField.forceActiveFocus);
} }
@@ -882,7 +889,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
root.keyboardHighlightIndex = -1; root.keyboardHighlightIndex = -1;
root.currentIndex = childDelegate.modelData.tabIndex; root.tabChangeRequested(childDelegate.modelData.tabIndex);
Qt.callLater(searchField.forceActiveFocus); Qt.callLater(searchField.forceActiveFocus);
} }
} }

View File

@@ -38,11 +38,10 @@ DankModal {
isClosing = false; isClosing = false;
resetContent(); resetContent();
spotlightOpen = true; spotlightOpen = true;
if (spotlightContent?.appLauncher)
spotlightContent.appLauncher.ensureInitialized();
open(); open();
Qt.callLater(() => { Qt.callLater(() => {
if (spotlightContent?.appLauncher)
spotlightContent.appLauncher.ensureInitialized();
if (spotlightContent?.searchField) if (spotlightContent?.searchField)
spotlightContent.searchField.forceActiveFocus(); spotlightContent.searchField.forceActiveFocus();
}); });
@@ -53,15 +52,14 @@ DankModal {
isClosing = false; isClosing = false;
resetContent(); resetContent();
spotlightOpen = true; spotlightOpen = true;
if (spotlightContent?.appLauncher) {
spotlightContent.appLauncher.ensureInitialized();
spotlightContent.appLauncher.searchQuery = query;
}
if (spotlightContent?.searchField) if (spotlightContent?.searchField)
spotlightContent.searchField.text = query; spotlightContent.searchField.text = query;
open(); open();
Qt.callLater(() => { Qt.callLater(() => {
if (spotlightContent?.appLauncher) {
spotlightContent.appLauncher.ensureInitialized();
spotlightContent.appLauncher.searchQuery = query;
}
if (spotlightContent?.searchField) if (spotlightContent?.searchField)
spotlightContent.searchField.forceActiveFocus(); spotlightContent.searchField.forceActiveFocus();
}); });

View File

@@ -11,6 +11,7 @@ FloatingWindow {
property string wifiPasswordInput: "" property string wifiPasswordInput: ""
property string wifiUsernameInput: "" property string wifiUsernameInput: ""
property bool requiresEnterprise: false property bool requiresEnterprise: false
property bool isHiddenNetwork: false
property string wifiAnonymousIdentityInput: "" property string wifiAnonymousIdentityInput: ""
property string wifiDomainInput: "" property string wifiDomainInput: ""
@@ -32,7 +33,6 @@ FloatingWindow {
readonly property bool showPasswordField: fieldsInfo.length === 0 readonly property bool showPasswordField: fieldsInfo.length === 0
readonly property bool showAnonField: requiresEnterprise && !isVpnPrompt readonly property bool showAnonField: requiresEnterprise && !isVpnPrompt
readonly property bool showDomainField: requiresEnterprise && !isVpnPrompt readonly property bool showDomainField: requiresEnterprise && !isVpnPrompt
readonly property bool showShowPasswordCheckbox: fieldsInfo.length === 0
readonly property bool showSavePasswordCheckbox: (isVpnPrompt || fieldsInfo.length > 0) && promptReason !== "pkcs11" readonly property bool showSavePasswordCheckbox: (isVpnPrompt || fieldsInfo.length > 0) && promptReason !== "pkcs11"
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
@@ -44,6 +44,8 @@ FloatingWindow {
property int calculatedHeight: { property int calculatedHeight: {
let h = headerHeight + buttonRowHeight + Theme.spacingL * 2; let h = headerHeight + buttonRowHeight + Theme.spacingL * 2;
h += fieldsInfo.length * inputFieldWithSpacing; h += fieldsInfo.length * inputFieldWithSpacing;
if (isHiddenNetwork)
h += inputFieldWithSpacing;
if (showUsernameField) if (showUsernameField)
h += inputFieldWithSpacing; h += inputFieldWithSpacing;
if (showPasswordField) if (showPasswordField)
@@ -52,8 +54,6 @@ FloatingWindow {
h += inputFieldWithSpacing; h += inputFieldWithSpacing;
if (showDomainField) if (showDomainField)
h += inputFieldWithSpacing; h += inputFieldWithSpacing;
if (showShowPasswordCheckbox)
h += checkboxRowHeight;
if (showSavePasswordCheckbox) if (showSavePasswordCheckbox)
h += checkboxRowHeight; h += checkboxRowHeight;
return h; return h;
@@ -68,6 +68,10 @@ FloatingWindow {
} }
return; return;
} }
if (isHiddenNetwork) {
ssidInput.forceActiveFocus();
return;
}
if (requiresEnterprise && !isVpnPrompt) { if (requiresEnterprise && !isVpnPrompt) {
usernameInput.forceActiveFocus(); usernameInput.forceActiveFocus();
return; return;
@@ -82,6 +86,7 @@ FloatingWindow {
wifiAnonymousIdentityInput = ""; wifiAnonymousIdentityInput = "";
wifiDomainInput = ""; wifiDomainInput = "";
isPromptMode = false; isPromptMode = false;
isHiddenNetwork = false;
promptToken = ""; promptToken = "";
promptReason = ""; promptReason = "";
promptFields = []; promptFields = [];
@@ -100,6 +105,30 @@ FloatingWindow {
Qt.callLater(focusFirstField); Qt.callLater(focusFirstField);
} }
function showHidden() {
wifiPasswordSSID = "";
wifiPasswordInput = "";
wifiUsernameInput = "";
wifiAnonymousIdentityInput = "";
wifiDomainInput = "";
isPromptMode = false;
isHiddenNetwork = true;
promptToken = "";
promptReason = "";
promptFields = [];
promptSetting = "";
isVpnPrompt = false;
connectionName = "";
vpnServiceType = "";
connectionType = "";
fieldsInfo = [];
secretValues = {};
requiresEnterprise = false;
visible = true;
Qt.callLater(focusFirstField);
}
function showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fInfo) { function showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fInfo) {
isPromptMode = true; isPromptMode = true;
promptToken = token; promptToken = token;
@@ -184,8 +213,9 @@ FloatingWindow {
} }
NetworkService.submitCredentials(promptToken, secrets, savePasswordCheckbox.checked); NetworkService.submitCredentials(promptToken, secrets, savePasswordCheckbox.checked);
} else { } else {
const ssid = isHiddenNetwork ? ssidInput.text : wifiPasswordSSID;
const username = requiresEnterprise ? usernameInput.text : ""; const username = requiresEnterprise ? usernameInput.text : "";
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username, wifiAnonymousIdentityInput, wifiDomainInput); NetworkService.connectToWifi(ssid, passwordInput.text, username, wifiAnonymousIdentityInput, wifiDomainInput, isHiddenNetwork);
} }
hide(); hide();
@@ -196,6 +226,8 @@ FloatingWindow {
passwordInput.text = ""; passwordInput.text = "";
if (requiresEnterprise) if (requiresEnterprise)
usernameInput.text = ""; usernameInput.text = "";
if (isHiddenNetwork)
ssidInput.text = "";
} }
function clearAndClose() { function clearAndClose() {
@@ -215,6 +247,8 @@ FloatingWindow {
return I18n.tr("Smartcard PIN"); return I18n.tr("Smartcard PIN");
if (isVpnPrompt) if (isVpnPrompt)
return I18n.tr("VPN Password"); return I18n.tr("VPN Password");
if (isHiddenNetwork)
return I18n.tr("Hidden Network");
return I18n.tr("Wi-Fi Password"); return I18n.tr("Wi-Fi Password");
} }
minimumSize: Qt.size(420, calculatedHeight) minimumSize: Qt.size(420, calculatedHeight)
@@ -236,6 +270,7 @@ FloatingWindow {
usernameInput.text = ""; usernameInput.text = "";
anonInput.text = ""; anonInput.text = "";
domainMatchInput.text = ""; domainMatchInput.text = "";
ssidInput.text = "";
for (var i = 0; i < dynamicFieldsRepeater.count; i++) { for (var i = 0; i < dynamicFieldsRepeater.count; i++) {
const item = dynamicFieldsRepeater.itemAt(i); const item = dynamicFieldsRepeater.itemAt(i);
if (item?.children[0]) if (item?.children[0])
@@ -296,6 +331,8 @@ FloatingWindow {
return I18n.tr("Smartcard Authentication"); return I18n.tr("Smartcard Authentication");
if (isVpnPrompt) if (isVpnPrompt)
return I18n.tr("Connect to VPN"); return I18n.tr("Connect to VPN");
if (isHiddenNetwork)
return I18n.tr("Connect to Hidden Network");
return I18n.tr("Connect to Wi-Fi"); return I18n.tr("Connect to Wi-Fi");
} }
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
@@ -315,6 +352,8 @@ FloatingWindow {
return I18n.tr("Enter credentials for ") + wifiPasswordSSID; return I18n.tr("Enter credentials for ") + wifiPasswordSSID;
if (isVpnPrompt) if (isVpnPrompt)
return I18n.tr("Enter password for ") + wifiPasswordSSID; return I18n.tr("Enter password for ") + wifiPasswordSSID;
if (isHiddenNetwork)
return I18n.tr("Enter network name and password");
const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for "); const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ");
return prefix + wifiPasswordSSID; return prefix + wifiPasswordSSID;
} }
@@ -357,6 +396,34 @@ FloatingWindow {
} }
} }
Rectangle {
width: parent.width
height: inputFieldHeight
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: ssidInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: ssidInput.activeFocus ? 2 : 1
visible: isHiddenNetwork
MouseArea {
anchors.fill: parent
onClicked: ssidInput.forceActiveFocus()
}
DankTextField {
id: ssidInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
placeholderText: I18n.tr("Network Name (SSID)")
backgroundColor: "transparent"
enabled: root.visible
keyNavigationTab: passwordInput
onAccepted: passwordInput.forceActiveFocus()
}
}
Repeater { Repeater {
id: dynamicFieldsRepeater id: dynamicFieldsRepeater
model: fieldsInfo model: fieldsInfo
@@ -377,7 +444,8 @@ FloatingWindow {
anchors.fill: parent anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText textColor: Theme.surfaceText
echoMode: modelData.isSecret ? TextInput.Password : TextInput.Normal showPasswordToggle: modelData.isSecret
echoMode: modelData.isSecret && !passwordVisible ? TextInput.Password : TextInput.Normal
placeholderText: getFieldLabel(modelData.name) placeholderText: getFieldLabel(modelData.name)
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.visible enabled: root.visible
@@ -479,7 +547,8 @@ FloatingWindow {
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText textColor: Theme.surfaceText
text: wifiPasswordInput text: wifiPasswordInput
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password showPasswordToggle: true
echoMode: passwordVisible ? TextInput.Normal : TextInput.Password
placeholderText: (requiresEnterprise && !isVpnPrompt) ? I18n.tr("Password") : "" placeholderText: (requiresEnterprise && !isVpnPrompt) ? I18n.tr("Password") : ""
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.visible enabled: root.visible
@@ -558,88 +627,43 @@ FloatingWindow {
} }
} }
Column { Row {
spacing: Theme.spacingS spacing: Theme.spacingS
width: parent.width visible: showSavePasswordCheckbox
Row { Rectangle {
spacing: Theme.spacingS id: savePasswordCheckbox
visible: showShowPasswordCheckbox
Rectangle { property bool checked: true
id: showPasswordCheckbox
property bool checked: false width: 20
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Theme.outlineButton
border.width: 2
width: 20 DankIcon {
height: 20 anchors.centerIn: parent
radius: 4 name: "check"
color: checked ? Theme.primary : "transparent" size: 12
border.color: checked ? Theme.primary : Theme.outlineButton color: Theme.background
border.width: 2 visible: parent.checked
DankIcon {
anchors.centerIn: parent
name: "check"
size: 12
color: Theme.background
visible: parent.checked
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: showPasswordCheckbox.checked = !showPasswordCheckbox.checked
}
} }
StyledText { MouseArea {
text: I18n.tr("Show password") anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium hoverEnabled: true
color: Theme.surfaceText cursorShape: Qt.PointingHandCursor
anchors.verticalCenter: parent.verticalCenter onClicked: savePasswordCheckbox.checked = !savePasswordCheckbox.checked
} }
} }
Row { StyledText {
spacing: Theme.spacingS text: I18n.tr("Save password")
visible: showSavePasswordCheckbox font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
Rectangle { anchors.verticalCenter: parent.verticalCenter
id: savePasswordCheckbox
property bool checked: false
width: 20
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Theme.outlineButton
border.width: 2
DankIcon {
anchors.centerIn: parent
name: "check"
size: 12
color: Theme.background
visible: parent.checked
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: savePasswordCheckbox.checked = !savePasswordCheckbox.checked
}
}
StyledText {
text: I18n.tr("Save password")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
} }
} }
@@ -696,6 +720,8 @@ FloatingWindow {
} }
if (isVpnPrompt) if (isVpnPrompt)
return passwordInput.text.length > 0; return passwordInput.text.length > 0;
if (isHiddenNetwork)
return ssidInput.text.length > 0;
return requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0; return requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0;
} }
opacity: enabled ? 1 : 0.5 opacity: enabled ? 1 : 0.5

View File

@@ -40,6 +40,12 @@ Variants {
id: root id: root
anchors.fill: parent anchors.fill: parent
function encodeFileUrl(path) {
if (!path)
return "";
return "file://" + path.split('/').map(s => encodeURIComponent(s)).join('/');
}
property string source: SessionData.getMonitorWallpaper(modelData.name) || "" property string source: SessionData.getMonitorWallpaper(modelData.name) || ""
property bool isColorSource: source.startsWith("#") property bool isColorSource: source.startsWith("#")
@@ -83,7 +89,7 @@ Variants {
isInitialized = true; isInitialized = true;
return; return;
} }
const formattedSource = source.startsWith("file://") ? source : "file://" + source; const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
setWallpaperImmediate(formattedSource); setWallpaperImmediate(formattedSource);
isInitialized = true; isInitialized = true;
} }
@@ -100,7 +106,7 @@ Variants {
return; return;
} }
const formattedSource = source.startsWith("file://") ? source : "file://" + source; const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
if (!isInitialized || !currentWallpaper.source) { if (!isInitialized || !currentWallpaper.source) {
setWallpaperImmediate(formattedSource); setWallpaperImmediate(formattedSource);

View File

@@ -5,6 +5,9 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string iconName: "" property string iconName: ""
property string text: "" property string text: ""
property string secondaryText: "" property string secondaryText: ""
@@ -80,6 +83,7 @@ Rectangle {
color: isActive ? Theme.primaryText : Theme.surfaceText color: isActive ? Theme.primaryText : Theme.surfaceText
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
} }
Typography { Typography {
@@ -90,6 +94,7 @@ Rectangle {
visible: text.length > 0 visible: text.length > 0
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
} }
} }
} }

View File

@@ -6,6 +6,9 @@ import qs.Widgets
Row { Row {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var availableWidgets: [] property var availableWidgets: []
property Item popoutContent: null property Item popoutContent: null
@@ -103,6 +106,7 @@ Row {
color: Theme.surfaceText color: Theme.surfaceText
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
horizontalAlignment: Text.AlignLeft
} }
Typography { Typography {
@@ -111,6 +115,7 @@ Row {
color: Theme.outline color: Theme.outline
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
horizontalAlignment: Text.AlignLeft
} }
} }

View File

@@ -6,6 +6,9 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property bool editMode: false property bool editMode: false
signal powerButtonClicked signal powerButtonClicked

View File

@@ -5,6 +5,9 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string iconName: "" property string iconName: ""
property string text: "" property string text: ""

View File

@@ -8,6 +8,9 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property bool hasInputVolumeSliderInCC: { property bool hasInputVolumeSliderInCC: {
const widgets = SettingsData.controlCenterWidgets || []; const widgets = SettingsData.controlCenterWidgets || [];
return widgets.some(widget => widget.id === "inputVolumeSlider"); return widgets.some(widget => widget.id === "inputVolumeSlider");
@@ -119,6 +122,21 @@ Rectangle {
contentHeight: audioColumn.height contentHeight: audioColumn.height
clip: true clip: true
property int maxPinnedInputs: 3
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v)
if (typeof value === "string" && value.length > 0)
return [value]
return []
}
function getPinnedInputs() {
const pins = SettingsData.audioInputDevicePins || {}
return normalizePinList(pins["preferredInput"])
}
Column { Column {
id: audioColumn id: audioColumn
width: parent.width width: parent.width
@@ -130,16 +148,20 @@ Rectangle {
const nodes = Pipewire.nodes.values.filter(node => { const nodes = Pipewire.nodes.values.filter(node => {
return node.audio && !node.isSink && !node.isStream; return node.audio && !node.isSink && !node.isStream;
}); });
const pins = SettingsData.audioInputDevicePins || {}; const pinnedList = audioContent.getPinnedInputs();
const pinnedName = pins["preferredInput"];
let sorted = [...nodes]; let sorted = [...nodes];
sorted.sort((a, b) => { sorted.sort((a, b) => {
// Pinned device first // Pinned device first
if (a.name === pinnedName && b.name !== pinnedName) const aPinnedIndex = pinnedList.indexOf(a.name)
return -1; const bPinnedIndex = pinnedList.indexOf(b.name)
if (b.name === pinnedName && a.name !== pinnedName) if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
return 1; if (aPinnedIndex === -1)
return 1
if (bPinnedIndex === -1)
return -1
return aPinnedIndex - bPinnedIndex
}
// Then active device // Then active device
if (a === AudioService.source && b !== AudioService.source) if (a === AudioService.source && b !== AudioService.source)
return -1; return -1;
@@ -198,6 +220,7 @@ Rectangle {
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
} }
StyledText { StyledText {
@@ -207,6 +230,7 @@ Rectangle {
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
} }
} }
} }
@@ -219,7 +243,7 @@ Rectangle {
height: 28 height: 28
radius: height / 2 radius: height / 2
color: { color: {
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name; const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05); return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
} }
@@ -232,7 +256,7 @@ Rectangle {
name: "push_pin" name: "push_pin"
size: 16 size: 16
color: { color: {
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name; const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
return isThisDevicePinned ? Theme.primary : Theme.surfaceText; return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -240,12 +264,12 @@ Rectangle {
StyledText { StyledText {
text: { text: {
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name; const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin"); return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin");
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: { color: {
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name; const isThisDevicePinned = audioContent.getPinnedInputs().includes(modelData.name);
return isThisDevicePinned ? Theme.primary : Theme.surfaceText; return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -256,16 +280,24 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {})); const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {}))
const isCurrentlyPinned = pins["preferredInput"] === modelData.name; let pinnedList = audioContent.normalizePinList(pins["preferredInput"])
const pinIndex = pinnedList.indexOf(modelData.name)
if (isCurrentlyPinned) { if (pinIndex !== -1) {
delete pins["preferredInput"]; pinnedList.splice(pinIndex, 1)
} else { } else {
pins["preferredInput"] = modelData.name; pinnedList.unshift(modelData.name)
if (pinnedList.length > audioContent.maxPinnedInputs)
pinnedList = pinnedList.slice(0, audioContent.maxPinnedInputs)
} }
SettingsData.set("audioInputDevicePins", pins); if (pinnedList.length > 0)
pins["preferredInput"] = pinnedList
else
delete pins["preferredInput"]
SettingsData.set("audioInputDevicePins", pins)
} }
} }
} }

View File

@@ -8,6 +8,9 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property bool hasVolumeSliderInCC: { property bool hasVolumeSliderInCC: {
const widgets = SettingsData.controlCenterWidgets || []; const widgets = SettingsData.controlCenterWidgets || [];
return widgets.some(widget => widget.id === "volumeSlider"); return widgets.some(widget => widget.id === "volumeSlider");
@@ -129,6 +132,21 @@ Rectangle {
contentHeight: audioColumn.height contentHeight: audioColumn.height
clip: true clip: true
property int maxPinnedOutputs: 3
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v)
if (typeof value === "string" && value.length > 0)
return [value]
return []
}
function getPinnedOutputs() {
const pins = SettingsData.audioOutputDevicePins || {}
return normalizePinList(pins["preferredOutput"])
}
Column { Column {
id: audioColumn id: audioColumn
width: parent.width width: parent.width
@@ -140,16 +158,20 @@ Rectangle {
const nodes = Pipewire.nodes.values.filter(node => { const nodes = Pipewire.nodes.values.filter(node => {
return node.audio && node.isSink && !node.isStream; return node.audio && node.isSink && !node.isStream;
}); });
const pins = SettingsData.audioOutputDevicePins || {}; const pinnedList = audioContent.getPinnedOutputs();
const pinnedName = pins["preferredOutput"];
let sorted = [...nodes]; let sorted = [...nodes];
sorted.sort((a, b) => { sorted.sort((a, b) => {
// Pinned device first // Pinned device first
if (a.name === pinnedName && b.name !== pinnedName) const aPinnedIndex = pinnedList.indexOf(a.name)
return -1; const bPinnedIndex = pinnedList.indexOf(b.name)
if (b.name === pinnedName && a.name !== pinnedName) if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
return 1; if (aPinnedIndex === -1)
return 1
if (bPinnedIndex === -1)
return -1
return aPinnedIndex - bPinnedIndex
}
// Then active device // Then active device
if (a === AudioService.sink && b !== AudioService.sink) if (a === AudioService.sink && b !== AudioService.sink)
return -1; return -1;
@@ -210,6 +232,7 @@ Rectangle {
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
} }
StyledText { StyledText {
@@ -219,6 +242,7 @@ Rectangle {
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
} }
} }
} }
@@ -231,7 +255,7 @@ Rectangle {
height: 28 height: 28
radius: height / 2 radius: height / 2
color: { color: {
const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name; const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05); return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
} }
@@ -244,7 +268,7 @@ Rectangle {
name: "push_pin" name: "push_pin"
size: 16 size: 16
color: { color: {
const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name; const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
return isThisDevicePinned ? Theme.primary : Theme.surfaceText; return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -252,12 +276,12 @@ Rectangle {
StyledText { StyledText {
text: { text: {
const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name; const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin"); return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin");
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: { color: {
const isThisDevicePinned = (SettingsData.audioOutputDevicePins || {})["preferredOutput"] === modelData.name; const isThisDevicePinned = audioContent.getPinnedOutputs().includes(modelData.name);
return isThisDevicePinned ? Theme.primary : Theme.surfaceText; return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -268,16 +292,24 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {})); const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {}))
const isCurrentlyPinned = pins["preferredOutput"] === modelData.name; let pinnedList = audioContent.normalizePinList(pins["preferredOutput"])
const pinIndex = pinnedList.indexOf(modelData.name)
if (isCurrentlyPinned) { if (pinIndex !== -1) {
delete pins["preferredOutput"]; pinnedList.splice(pinIndex, 1)
} else { } else {
pins["preferredOutput"] = modelData.name; pinnedList.unshift(modelData.name)
if (pinnedList.length > audioContent.maxPinnedOutputs)
pinnedList = pinnedList.slice(0, audioContent.maxPinnedOutputs)
} }
SettingsData.set("audioOutputDevicePins", pins); if (pinnedList.length > 0)
pins["preferredOutput"] = pinnedList
else
delete pins["preferredOutput"]
SettingsData.set("audioOutputDevicePins", pins)
} }
} }
} }

View File

@@ -5,6 +5,11 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2 implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
@@ -110,6 +115,7 @@ Rectangle {
visible: text.length > 0 visible: text.length > 0
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
horizontalAlignment: Text.AlignLeft
} }
} }
} }
@@ -249,6 +255,7 @@ Rectangle {
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.8) color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.8)
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: parent.width width: parent.width
horizontalAlignment: Text.AlignLeft
} }
} }
} }

View File

@@ -6,6 +6,9 @@ import qs.Widgets
Item { Item {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var device: null property var device: null
property bool modalVisible: false property bool modalVisible: false
property var parentItem property var parentItem

View File

@@ -10,6 +10,9 @@ import qs.Modals
Rectangle { Rectangle {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
implicitHeight: { implicitHeight: {
if (height > 0) { if (height > 0) {
return height return height
@@ -147,6 +150,21 @@ Rectangle {
contentHeight: bluetoothColumn.height contentHeight: bluetoothColumn.height
clip: true clip: true
property int maxPinnedDevices: 3
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v)
if (typeof value === "string" && value.length > 0)
return [value]
return []
}
function getPinnedDevices() {
const pins = SettingsData.bluetoothDevicePins || {}
return normalizePinList(pins["preferredDevice"])
}
Column { Column {
id: bluetoothColumn id: bluetoothColumn
width: parent.width width: parent.width
@@ -159,14 +177,18 @@ Rectangle {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices) if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return [] return []
const pins = SettingsData.bluetoothDevicePins || {} const pinnedList = bluetoothContent.getPinnedDevices()
const pinnedAddr = pins["preferredDevice"]
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))] let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
devices.sort((a, b) => { devices.sort((a, b) => {
// Pinned device first // Pinned device first
if (a.address === pinnedAddr && b.address !== pinnedAddr) return -1 const aPinnedIndex = pinnedList.indexOf(a.address)
if (b.address === pinnedAddr && a.address !== pinnedAddr) return 1 const bPinnedIndex = pinnedList.indexOf(b.address)
if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
if (aPinnedIndex === -1) return 1
if (bPinnedIndex === -1) return -1
return aPinnedIndex - bPinnedIndex
}
// Then connected devices // Then connected devices
if (a.connected && !b.connected) return -1 if (a.connected && !b.connected) return -1
if (!a.connected && b.connected) return 1 if (!a.connected && b.connected) return 1
@@ -237,6 +259,7 @@ Rectangle {
font.weight: modelData.connected ? Font.Medium : Font.Normal font.weight: modelData.connected ? Font.Medium : Font.Normal
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
horizontalAlignment: Text.AlignLeft
} }
Row { Row {
@@ -298,7 +321,7 @@ Rectangle {
height: 28 height: 28
radius: height / 2 radius: height / 2
color: { color: {
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address)
return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05) return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05)
} }
@@ -311,7 +334,7 @@ Rectangle {
name: "push_pin" name: "push_pin"
size: 16 size: 16
color: { color: {
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address)
return isThisDevicePinned ? Theme.primary : Theme.surfaceText return isThisDevicePinned ? Theme.primary : Theme.surfaceText
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -319,12 +342,12 @@ Rectangle {
StyledText { StyledText {
text: { text: {
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address)
return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin") return isThisDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin")
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: { color: {
const isThisDevicePinned = (SettingsData.bluetoothDevicePins || {})["preferredDevice"] === modelData.address const isThisDevicePinned = bluetoothContent.getPinnedDevices().includes(modelData.address)
return isThisDevicePinned ? Theme.primary : Theme.surfaceText return isThisDevicePinned ? Theme.primary : Theme.surfaceText
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -336,14 +359,22 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.bluetoothDevicePins || {})) const pins = JSON.parse(JSON.stringify(SettingsData.bluetoothDevicePins || {}))
const isCurrentlyPinned = pins["preferredDevice"] === modelData.address let pinnedList = bluetoothContent.normalizePinList(pins["preferredDevice"])
const pinIndex = pinnedList.indexOf(modelData.address)
if (isCurrentlyPinned) { if (pinIndex !== -1) {
delete pins["preferredDevice"] pinnedList.splice(pinIndex, 1)
} else { } else {
pins["preferredDevice"] = modelData.address pinnedList.unshift(modelData.address)
if (pinnedList.length > bluetoothContent.maxPinnedDevices)
pinnedList = pinnedList.slice(0, bluetoothContent.maxPinnedDevices)
} }
if (pinnedList.length > 0)
pins["preferredDevice"] = pinnedList
else
delete pins["preferredDevice"]
SettingsData.set("bluetoothDevicePins", pins) SettingsData.set("bluetoothDevicePins", pins)
} }
} }
@@ -463,6 +494,7 @@ Rectangle {
color: Theme.surfaceText color: Theme.surfaceText
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
horizontalAlignment: Text.AlignLeft
} }
Row { Row {

View File

@@ -7,6 +7,9 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string initialDeviceName: "" property string initialDeviceName: ""
property string instanceId: "" property string instanceId: ""
property string screenName: "" property string screenName: ""
@@ -303,6 +306,7 @@ Rectangle {
font.weight: modelData.name === currentDeviceName ? Font.Medium : Font.Normal font.weight: modelData.name === currentDeviceName ? Font.Medium : Font.Normal
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
horizontalAlignment: Text.AlignLeft
} }
StyledText { StyledText {
@@ -311,6 +315,7 @@ Rectangle {
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
horizontalAlignment: Text.AlignLeft
} }
StyledText { StyledText {
@@ -328,6 +333,7 @@ Rectangle {
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
horizontalAlignment: Text.AlignLeft
} }
} }
} }

View File

@@ -6,6 +6,9 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string currentMountPath: "/" property string currentMountPath: "/"
property string instanceId: "" property string instanceId: ""
@@ -128,6 +131,7 @@ Rectangle {
font.weight: modelData.mount === currentMountPath ? Font.Medium : Font.Normal font.weight: modelData.mount === currentMountPath ? Font.Medium : Font.Normal
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
horizontalAlignment: Text.AlignLeft
} }
StyledText { StyledText {
@@ -137,6 +141,7 @@ Rectangle {
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
visible: modelData.mount !== "/" visible: modelData.mount !== "/"
horizontalAlignment: Text.AlignLeft
} }
StyledText { StyledText {
@@ -145,6 +150,7 @@ Rectangle {
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
horizontalAlignment: Text.AlignLeft
} }
} }
} }

View File

@@ -9,6 +9,9 @@ import qs.Modals
Rectangle { Rectangle {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
implicitHeight: { implicitHeight: {
if (height > 0) { if (height > 0) {
return height; return height;
@@ -34,6 +37,10 @@ Rectangle {
NetworkService.removeRef(); NetworkService.removeRef();
} }
property bool hasEthernetAvailable: (NetworkService.ethernetDevices?.length ?? 0) > 0
property bool hasWifiAvailable: (NetworkService.wifiDevices?.length ?? 0) > 0
property bool hasBothConnectionTypes: hasEthernetAvailable && hasWifiAvailable
property int currentPreferenceIndex: { property int currentPreferenceIndex: {
if (DMSService.apiVersion < 5) { if (DMSService.apiVersion < 5) {
return 1; return 1;
@@ -43,19 +50,24 @@ Rectangle {
return 1; return 1;
} }
const pref = NetworkService.userPreference; if (!hasEthernetAvailable) {
const status = NetworkService.networkStatus; return 1;
let index = 1;
if (pref === "ethernet") {
index = 0;
} else if (pref === "wifi") {
index = 1;
} else {
index = status === "ethernet" ? 0 : 1;
} }
return index; if (!hasWifiAvailable) {
return 0;
}
const pref = NetworkService.userPreference;
const status = NetworkService.networkStatus;
if (pref === "ethernet") {
return 0;
}
if (pref === "wifi") {
return 1;
}
return status === "ethernet" ? 0 : 1;
} }
Row { Row {
@@ -114,7 +126,7 @@ Rectangle {
DankButtonGroup { DankButtonGroup {
id: preferenceControls id: preferenceControls
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.backend === "networkmanager" && DMSService.apiVersion > 10 visible: hasBothConnectionTypes && NetworkService.backend === "networkmanager" && DMSService.apiVersion > 10
buttonHeight: 28 buttonHeight: 28
textSize: Theme.fontSizeSmall textSize: Theme.fontSizeSmall
@@ -451,20 +463,39 @@ Rectangle {
contentHeight: wifiColumn.height contentHeight: wifiColumn.height
clip: true clip: true
property int maxPinnedNetworks: 3
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v)
if (typeof value === "string" && value.length > 0)
return [value]
return []
}
function getPinnedNetworks() {
const pins = SettingsData.wifiNetworkPins || {}
return normalizePinList(pins["preferredWifi"])
}
property var frozenNetworks: [] property var frozenNetworks: []
property bool menuOpen: false property bool menuOpen: false
property var sortedNetworks: { property var sortedNetworks: {
const ssid = NetworkService.currentWifiSSID; const ssid = NetworkService.currentWifiSSID;
const networks = NetworkService.wifiNetworks; const networks = NetworkService.wifiNetworks;
const pins = SettingsData.wifiNetworkPins || {}; const pinnedList = getPinnedNetworks()
const pinnedSSID = pins["preferredWifi"];
let sorted = [...networks]; let sorted = [...networks];
sorted.sort((a, b) => { sorted.sort((a, b) => {
if (a.ssid === pinnedSSID && b.ssid !== pinnedSSID) const aPinnedIndex = pinnedList.indexOf(a.ssid)
return -1; const bPinnedIndex = pinnedList.indexOf(b.ssid)
if (b.ssid === pinnedSSID && a.ssid !== pinnedSSID) if (aPinnedIndex !== -1 || bPinnedIndex !== -1) {
return 1; if (aPinnedIndex === -1)
return 1
if (bPinnedIndex === -1)
return -1
return aPinnedIndex - bPinnedIndex
}
if (a.ssid === ssid) if (a.ssid === ssid)
return -1; return -1;
if (b.ssid === ssid) if (b.ssid === ssid)
@@ -613,7 +644,7 @@ Rectangle {
height: 28 height: 28
radius: height / 2 radius: height / 2
color: { color: {
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid; const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
return isThisNetworkPinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05); return isThisNetworkPinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
} }
@@ -626,7 +657,7 @@ Rectangle {
name: "push_pin" name: "push_pin"
size: 16 size: 16
color: { color: {
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid; const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
return isThisNetworkPinned ? Theme.primary : Theme.surfaceText; return isThisNetworkPinned ? Theme.primary : Theme.surfaceText;
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -634,12 +665,12 @@ Rectangle {
StyledText { StyledText {
text: { text: {
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid; const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
return isThisNetworkPinned ? I18n.tr("Pinned") : I18n.tr("Pin"); return isThisNetworkPinned ? I18n.tr("Pinned") : I18n.tr("Pin");
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: { color: {
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid; const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
return isThisNetworkPinned ? Theme.primary : Theme.surfaceText; return isThisNetworkPinned ? Theme.primary : Theme.surfaceText;
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -650,16 +681,24 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {})); const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}))
const isCurrentlyPinned = pins["preferredWifi"] === modelData.ssid; let pinnedList = wifiContent.normalizePinList(pins["preferredWifi"])
const pinIndex = pinnedList.indexOf(modelData.ssid)
if (isCurrentlyPinned) { if (pinIndex !== -1) {
delete pins["preferredWifi"]; pinnedList.splice(pinIndex, 1)
} else { } else {
pins["preferredWifi"] = modelData.ssid; pinnedList.unshift(modelData.ssid)
if (pinnedList.length > wifiContent.maxPinnedNetworks)
pinnedList = pinnedList.slice(0, wifiContent.maxPinnedNetworks)
} }
SettingsData.set("wifiNetworkPins", pins); if (pinnedList.length > 0)
pins["preferredWifi"] = pinnedList
else
delete pins["preferredWifi"]
SettingsData.set("wifiNetworkPins", pins)
} }
} }
} }
@@ -675,8 +714,8 @@ Rectangle {
if (modelData.secured && !modelData.saved) { if (modelData.secured && !modelData.saved) {
if (DMSService.apiVersion >= 7) { if (DMSService.apiVersion >= 7) {
NetworkService.connectToWifi(modelData.ssid); NetworkService.connectToWifi(modelData.ssid);
} else if (PopoutService.wifiPasswordModal) { } else {
PopoutService.wifiPasswordModal.show(modelData.ssid); PopoutService.showWifiPasswordModal(modelData.ssid);
} }
} else { } else {
NetworkService.connectToWifi(modelData.ssid); NetworkService.connectToWifi(modelData.ssid);
@@ -737,8 +776,8 @@ Rectangle {
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) { if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
if (DMSService.apiVersion >= 7) { if (DMSService.apiVersion >= 7) {
NetworkService.connectToWifi(networkContextMenu.currentSSID); NetworkService.connectToWifi(networkContextMenu.currentSSID);
} else if (PopoutService.wifiPasswordModal) { } else {
PopoutService.wifiPasswordModal.show(networkContextMenu.currentSSID); PopoutService.showWifiPasswordModal(networkContextMenu.currentSSID);
} }
} else { } else {
NetworkService.connectToWifi(networkContextMenu.currentSSID); NetworkService.connectToWifi(networkContextMenu.currentSSID);

View File

@@ -6,6 +6,9 @@ import qs.Widgets
Row { Row {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var defaultSink: AudioService.sink property var defaultSink: AudioService.sink
property color sliderTrackColor: "transparent" property color sliderTrackColor: "transparent"

View File

@@ -6,6 +6,9 @@ import qs.Widgets
Row { Row {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string deviceName: "" property string deviceName: ""
property string instanceId: "" property string instanceId: ""
property string screenName: "" property string screenName: ""

View File

@@ -1,12 +1,13 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string iconName: "" property string iconName: ""
property color iconColor: Theme.surfaceText property color iconColor: Theme.surfaceText
property string labelText: "" property string labelText: ""

View File

@@ -5,6 +5,9 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string iconName: "" property string iconName: ""
property color iconColor: Theme.surfaceText property color iconColor: Theme.surfaceText
property string primaryText: "" property string primaryText: ""
@@ -137,6 +140,7 @@ Rectangle {
font.weight: Font.Medium font.weight: Font.Medium
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
} }
StyledText { StyledText {
width: parent.width width: parent.width
@@ -146,6 +150,7 @@ Rectangle {
visible: text.length > 0 visible: text.length > 0
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
} }
} }

View File

@@ -1,8 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Widgets
Rectangle { Rectangle {
id: root id: root
@@ -24,6 +20,4 @@ Rectangle {
sourceComponent: root.content sourceComponent: root.content
asynchronous: true asynchronous: true
} }
} }

View File

@@ -5,6 +5,9 @@ import qs.Widgets
StyledRect { StyledRect {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string primaryMessage: "" property string primaryMessage: ""
property string secondaryMessage: "" property string secondaryMessage: ""
@@ -37,6 +40,7 @@ StyledRect {
color: Theme.warning color: Theme.warning
font.weight: Font.Medium font.weight: Font.Medium
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignLeft
} }
StyledText { StyledText {
@@ -45,6 +49,7 @@ StyledRect {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.warning color: Theme.warning
visible: text.length > 0 visible: text.length > 0
horizontalAlignment: Text.AlignLeft
} }
} }
} }

View File

@@ -6,6 +6,9 @@ import qs.Widgets
Row { Row {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var defaultSource: AudioService.source property var defaultSource: AudioService.source
property color sliderTrackColor: "transparent" property color sliderTrackColor: "transparent"

View File

@@ -6,6 +6,9 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property bool isActive: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn) property bool isActive: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn)
property bool enabled: BatteryService.batteryAvailable property bool enabled: BatteryService.batteryAvailable

View File

@@ -6,6 +6,9 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string mountPath: "/" property string mountPath: "/"
property string instanceId: "" property string instanceId: ""
@@ -84,6 +87,7 @@ Rectangle {
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideMiddle elide: Text.ElideMiddle
width: Math.min(implicitWidth, root.width - Theme.iconSizeSmall - Theme.spacingM) width: Math.min(implicitWidth, root.width - Theme.iconSizeSmall - Theme.spacingM)
horizontalAlignment: Text.AlignLeft
} }
StyledText { StyledText {

View File

@@ -5,6 +5,9 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string iconName: "" property string iconName: ""
property string text: "" property string text: ""
property bool isActive: false property bool isActive: false
@@ -90,6 +93,7 @@ Rectangle {
font.weight: Font.Medium font.weight: Font.Medium
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
} }
StyledText { StyledText {
@@ -100,6 +104,7 @@ Rectangle {
visible: text.length > 0 visible: text.length > 0
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.NoWrap wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
} }
} }
} }

View File

@@ -99,6 +99,8 @@ Item {
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
focusedScreenName = focusedWs?.monitor?.name || ""; focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) {
focusedScreenName = DwlService.activeOutput;
} }
if (!focusedScreenName && barVariants.instances.length > 0) { if (!focusedScreenName && barVariants.instances.length > 0) {
@@ -126,6 +128,8 @@ Item {
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
focusedScreenName = focusedWs?.monitor?.name || ""; focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) {
focusedScreenName = DwlService.activeOutput;
} }
if (!focusedScreenName && barVariants.instances.length > 0) { if (!focusedScreenName && barVariants.instances.length > 0) {

View File

@@ -39,7 +39,7 @@ Item {
function getRealWorkspaces() { function getRealWorkspaces() {
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
if (!barWindow.screenName || !SettingsData.workspacesPerMonitor) { if (!barWindow.screenName || SettingsData.workspaceFollowFocus) {
return NiriService.getCurrentOutputWorkspaceNumbers(); return NiriService.getCurrentOutputWorkspaceNumbers();
} }
const workspaces = NiriService.allWorkspaces.filter(ws => ws.output === barWindow.screenName).map(ws => ws.idx + 1); const workspaces = NiriService.allWorkspaces.filter(ws => ws.output === barWindow.screenName).map(ws => ws.idx + 1);
@@ -47,7 +47,7 @@ Item {
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
const workspaces = Hyprland.workspaces?.values || []; const workspaces = Hyprland.workspaces?.values || [];
if (!barWindow.screenName || !SettingsData.workspacesPerMonitor) { if (!barWindow.screenName || SettingsData.workspaceFollowFocus) {
const sorted = workspaces.slice().sort((a, b) => a.id - b.id); const sorted = workspaces.slice().sort((a, b) => a.id - b.id);
const filtered = sorted.filter(ws => ws.id > -1); const filtered = sorted.filter(ws => ws.id > -1);
return filtered.length > 0 ? filtered : [ return filtered.length > 0 ? filtered : [
@@ -91,7 +91,7 @@ Item {
} }
]; ];
if (!barWindow.screenName || !SettingsData.workspacesPerMonitor) { if (!barWindow.screenName || SettingsData.workspaceFollowFocus) {
return workspaces.slice().sort((a, b) => a.num - b.num); return workspaces.slice().sort((a, b) => a.num - b.num);
} }
@@ -107,7 +107,7 @@ Item {
function getCurrentWorkspace() { function getCurrentWorkspace() {
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
if (!barWindow.screenName || !SettingsData.workspacesPerMonitor) { if (!barWindow.screenName || SettingsData.workspaceFollowFocus) {
return NiriService.getCurrentWorkspaceNumber(); return NiriService.getCurrentWorkspaceNumber();
} }
const activeWs = NiriService.allWorkspaces.find(ws => ws.output === barWindow.screenName && ws.is_active); const activeWs = NiriService.allWorkspaces.find(ws => ws.output === barWindow.screenName && ws.is_active);
@@ -125,7 +125,7 @@ Item {
const activeTags = DwlService.getActiveTags(barWindow.screenName); const activeTags = DwlService.getActiveTags(barWindow.screenName);
return activeTags.length > 0 ? activeTags[0] : 0; return activeTags.length > 0 ? activeTags[0] : 0;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll) {
if (!barWindow.screenName || !SettingsData.workspacesPerMonitor) { if (!barWindow.screenName || SettingsData.workspaceFollowFocus) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
return focusedWs ? focusedWs.num : 1; return focusedWs ? focusedWs.num : 1;
} }

View File

@@ -24,10 +24,12 @@ BasePill {
property bool showMicPercent: widgetData?.showMicPercent !== undefined ? widgetData.showMicPercent : SettingsData.controlCenterShowMicPercent property bool showMicPercent: widgetData?.showMicPercent !== undefined ? widgetData.showMicPercent : SettingsData.controlCenterShowMicPercent
property bool showBatteryIcon: widgetData?.showBatteryIcon !== undefined ? widgetData.showBatteryIcon : SettingsData.controlCenterShowBatteryIcon property bool showBatteryIcon: widgetData?.showBatteryIcon !== undefined ? widgetData.showBatteryIcon : SettingsData.controlCenterShowBatteryIcon
property bool showPrinterIcon: widgetData?.showPrinterIcon !== undefined ? widgetData.showPrinterIcon : SettingsData.controlCenterShowPrinterIcon property bool showPrinterIcon: widgetData?.showPrinterIcon !== undefined ? widgetData.showPrinterIcon : SettingsData.controlCenterShowPrinterIcon
property bool showScreenSharingIcon: widgetData?.showScreenSharingIcon !== undefined ? widgetData.showScreenSharingIcon : SettingsData.controlCenterShowScreenSharingIcon
property real touchpadThreshold: 100 property real touchpadThreshold: 100
property real micAccumulator: 0 property real micAccumulator: 0
property real volumeAccumulator: 0 property real volumeAccumulator: 0
property real brightnessAccumulator: 0 property real brightnessAccumulator: 0
readonly property real vIconSize: Theme.barIconSize(root.barThickness, -4)
Loader { Loader {
active: root.showPrinterIcon active: root.showPrinterIcon
@@ -213,7 +215,25 @@ BasePill {
} }
function hasNoVisibleIcons() { function hasNoVisibleIcons() {
return !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon && !root.showVpnIcon && !root.showBrightnessIcon && !root.showMicIcon && !root.showBatteryIcon && !root.showPrinterIcon; if (root.showScreenSharingIcon && NiriService.hasCasts)
return false;
if (root.showNetworkIcon && NetworkService.networkAvailable)
return false;
if (root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected)
return false;
if (root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled)
return false;
if (root.showAudioIcon)
return false;
if (root.showMicIcon)
return false;
if (root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice())
return false;
if (root.showBatteryIcon && BatteryService.batteryAvailable)
return false;
if (root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs())
return false;
return true;
} }
content: Component { content: Component {
@@ -224,48 +244,74 @@ BasePill {
Column { Column {
id: controlColumn id: controlColumn
visible: root.isVerticalOrientation visible: root.isVerticalOrientation
anchors.centerIn: parent width: root.vIconSize
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankIcon { Item {
name: root.getNetworkIconName() width: root.vIconSize
size: Theme.barIconSize(root.barThickness, -4) height: root.vIconSize
color: root.getNetworkIconColor() visible: root.showScreenSharingIcon && NiriService.hasCasts
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: "screen_record"
size: root.vIconSize
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent
}
}
Item {
width: root.vIconSize
height: root.vIconSize
visible: root.showNetworkIcon && NetworkService.networkAvailable visible: root.showNetworkIcon && NetworkService.networkAvailable
DankIcon {
name: root.getNetworkIconName()
size: root.vIconSize
color: root.getNetworkIconColor()
anchors.centerIn: parent
}
} }
DankIcon { Item {
name: "vpn_lock" width: root.vIconSize
size: Theme.barIconSize(root.barThickness, -4) height: root.vIconSize
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
DankIcon {
name: "vpn_lock"
size: root.vIconSize
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent
}
} }
DankIcon { Item {
name: "bluetooth" width: root.vIconSize
size: Theme.barIconSize(root.barThickness, -4) height: root.vIconSize
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
DankIcon {
name: "bluetooth"
size: root.vIconSize
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent
}
} }
Rectangle { Item {
width: audioIconV.implicitWidth + 4 width: root.vIconSize
height: audioIconV.implicitHeight + (root.showAudioPercent ? audioPercentV.implicitHeight : 0) + 4 height: root.vIconSize + (root.showAudioPercent ? audioPercentV.implicitHeight + 2 : 0)
color: "transparent"
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showAudioIcon visible: root.showAudioIcon
DankIcon { DankIcon {
id: audioIconV id: audioIconV
name: root.getVolumeIconName() name: root.getVolumeIconName()
size: Theme.barIconSize(root.barThickness, -4) size: root.vIconSize
color: Theme.widgetIconColor color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: 2
} }
StyledText { StyledText {
@@ -292,21 +338,18 @@ BasePill {
} }
} }
Rectangle { Item {
width: micIconV.implicitWidth + 4 width: root.vIconSize
height: micIconV.implicitHeight + (root.showAudioPercent ? micPercentV.implicitHeight : 0) + 4 height: root.vIconSize + (root.showMicPercent ? micPercentV.implicitHeight + 2 : 0)
color: "transparent"
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showMicIcon visible: root.showMicIcon
DankIcon { DankIcon {
id: micIconV id: micIconV
name: root.getMicIconName() name: root.getMicIconName()
size: Theme.barIconSize(root.barThickness, -4) size: root.vIconSize
color: root.getMicIconColor() color: root.getMicIconColor()
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: 2
} }
StyledText { StyledText {
@@ -333,21 +376,18 @@ BasePill {
} }
} }
Rectangle { Item {
width: brightnessIconV.implicitWidth + 4 width: root.vIconSize
height: brightnessIconV.implicitHeight + (root.showBrightnessPercent ? brightnessPercentV.implicitHeight : 0) + 4 height: root.vIconSize + (root.showBrightnessPercent ? brightnessPercentV.implicitHeight + 2 : 0)
color: "transparent"
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice() visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
DankIcon { DankIcon {
id: brightnessIconV id: brightnessIconV
name: root.getBrightnessIconName() name: root.getBrightnessIconName()
size: Theme.barIconSize(root.barThickness, -4) size: root.vIconSize
color: Theme.widgetIconColor color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: 2
} }
StyledText { StyledText {
@@ -371,28 +411,43 @@ BasePill {
} }
} }
DankIcon { Item {
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable) width: root.vIconSize
size: Theme.barIconSize(root.barThickness, -4) height: root.vIconSize
color: root.getBatteryIconColor()
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showBatteryIcon && BatteryService.batteryAvailable visible: root.showBatteryIcon && BatteryService.batteryAvailable
DankIcon {
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: root.vIconSize
color: root.getBatteryIconColor()
anchors.centerIn: parent
}
} }
DankIcon { Item {
name: "print" width: root.vIconSize
size: Theme.barIconSize(root.barThickness, -4) height: root.vIconSize
color: Theme.primary
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs() visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
DankIcon {
name: "print"
size: root.vIconSize
color: Theme.primary
anchors.centerIn: parent
}
} }
DankIcon { Item {
name: "settings" width: root.vIconSize
size: Theme.barIconSize(root.barThickness, -4) height: root.vIconSize
color: root.isActive ? Theme.primary : Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
visible: root.hasNoVisibleIcons() visible: root.hasNoVisibleIcons()
DankIcon {
name: "settings"
size: root.vIconSize
color: root.isActive ? Theme.primary : Theme.widgetIconColor
anchors.centerIn: parent
}
} }
} }
@@ -402,6 +457,14 @@ BasePill {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankIcon {
name: "screen_record"
size: Theme.barIconSize(root.barThickness, -4)
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: root.showScreenSharingIcon && NiriService.hasCasts
}
DankIcon { DankIcon {
id: networkIcon id: networkIcon
name: root.getNetworkIconName() name: root.getNetworkIconName()

Some files were not shown because too many files have changed in this diff Show More