1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-27 06:52:50 -05:00

Compare commits

...

113 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
bbedward
e6d289d48c workflow: update stable workflow to use GH app 2026-01-06 22:45:34 -05:00
bbedward
745d7f26ce cursor: create/update XResources for XWL apps 2026-01-06 22:06:01 -05:00
bbedward
ad43053b94 cursor: hypr, mango, and dankinstall support for configs 2026-01-06 20:35:22 -05:00
purian23
721700190b feat: DMS Cursor Control - Size & Theme in niri 2026-01-06 19:08:05 -05:00
bbedward
8c9c936d0e clipboard: add cliphist-migrate CLI 2026-01-06 16:49:18 -05:00
dms-ci[bot]
842bf6e3ff nix: update vendorHash for go.mod changes 2026-01-06 21:03:38 +00:00
bbedward
c1fbeb3f5e network: listen to NM Wired interface + use nmcli for route metrics
- Some other misc floating window change, too lazy to separate the
  commit
2026-01-06 16:01:28 -05:00
bbedward
c45eb2cccf plugins: ipc visibility conditions 2026-01-06 13:22:36 -05:00
bbedward
1b5abca83a launcher remove right key 2026-01-06 10:37:58 -05:00
bbedward
45818b202f launcher: support for plugins to define context menus
fixes #1279
2026-01-06 10:08:22 -05:00
Ethan Todd
1c8ce46f25 notifications: fix notifications being completely transient if history is disabled (#1284) 2026-01-06 09:39:33 -05:00
bbedward
f762f9ae49 theme: fix gtk apply button on empty file
fixes #1280
2026-01-05 21:56:50 -05:00
bbedward
4484f6bd61 launcher: built-in plugins, add settings search plugin with ? default
trigger
2026-01-05 21:46:12 -05:00
purian23
0076c45496 shell: dmsCoreApp updates 2026-01-05 20:31:06 -05:00
bbedward
ab071e12aa icons: fix transmission-gtk modded app ID again 2026-01-05 16:44:31 -05:00
bbedward
8386b40c50 launcher: F10 as alt for menu key 2026-01-05 14:58:50 -05:00
bbedward
03a985228d dankbar: add shadow option
fixes #916
2026-01-05 13:43:15 -05:00
bbedward
ef7d7ec13d desktop widgets: niri overview only option + grid on overlay when on
overview
2026-01-05 13:01:10 -05:00
bbedward
824792cca7 notifications: add support for none, count, app name, and full detail
for lock screen
fixes #557
2026-01-05 12:22:05 -05:00
bbedward
850e5b6572 session: handle hibernate error
fixes #308
2026-01-05 12:01:17 -05:00
bbedward
64310854a6 compositor+matugen: border override, hypr/mango layout overrides, new
templates, respect XDG paths
- Add Hyprland and MangoWC templates
- Add GUI gaps, window radius, and border thickness overrides for niri,
  Hyprland, and MangoWC
- Add replacement support in matugen templates for DATA_DIR, CACHE_DIR,
  CONFIG_DIR
fixes #1274
fixes #1273
2026-01-05 11:25:13 -05:00
bbedward
4005a55bf2 session: blockLoading true 2026-01-05 09:11:19 -05:00
bbedward
0236fe3276 session: fix persist on empty file 2026-01-05 09:07:00 -05:00
261 changed files with 23251 additions and 4868 deletions

View File

@@ -5,15 +5,27 @@ on:
tags: tags:
- "v*" - "v*"
permissions:
contents: write
jobs: jobs:
update-stable: update-stable:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
steps: steps:
- uses: actions/checkout@v4 - name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Push to stable branch - name: Push to stable branch
run: git push origin HEAD:refs/heads/stable --force env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:refs/heads/stable --force

2
.gitignore vendored
View File

@@ -108,3 +108,5 @@ bin/
# direnv # direnv
.envrc .envrc
.direnv/ .direnv/
quickshell/dms-plugins
__pycache__

View File

@@ -18,3 +18,6 @@ This file is more of a quick reference so I know what to account for before next
- Notification persistence & history - Notification persistence & history
- **BREAKING** vscode theme needs re-installed - **BREAKING** vscode theme needs re-installed
- dms doctor cmd - dms doctor cmd
- niri/hypr/mango gaps/window/border overrides
- settings search
- notification display ops on lock screen

View File

@@ -1,17 +1,29 @@
package main package main
import ( import (
"bytes"
"context" "context"
"encoding/base64"
"encoding/binary"
"encoding/json" "encoding/json"
"fmt" "fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io" "io"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"path/filepath"
"strconv" "strconv"
"syscall" "syscall"
"time" "time"
bolt "go.etcd.io/bbolt"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
@@ -144,6 +156,30 @@ var (
clipConfigEnabled bool clipConfigEnabled bool
) )
var clipExportCmd = &cobra.Command{
Use: "export [file]",
Short: "Export clipboard history to JSON",
Long: "Export clipboard history to JSON file. If no file specified, writes to stdout.",
Run: runClipExport,
}
var clipImportCmd = &cobra.Command{
Use: "import <file>",
Short: "Import clipboard history from JSON",
Long: "Import clipboard history from JSON file exported by 'dms cl export'.",
Args: cobra.ExactArgs(1),
Run: runClipImport,
}
var clipMigrateCmd = &cobra.Command{
Use: "cliphist-migrate [db-path]",
Short: "Migrate from cliphist",
Long: "Migrate clipboard history from cliphist. Uses default cliphist path if not specified.",
Run: runClipMigrate,
}
var clipMigrateDelete bool
func init() { func init() {
clipCopyCmd.Flags().BoolVarP(&clipCopyForeground, "foreground", "f", false, "Stay in foreground instead of forking") clipCopyCmd.Flags().BoolVarP(&clipCopyForeground, "foreground", "f", false, "Stay in foreground instead of forking")
clipCopyCmd.Flags().BoolVarP(&clipCopyPasteOnce, "paste-once", "o", false, "Exit after first paste") clipCopyCmd.Flags().BoolVarP(&clipCopyPasteOnce, "paste-once", "o", false, "Exit after first paste")
@@ -170,8 +206,10 @@ func init() {
clipWatchCmd.Flags().BoolVarP(&clipWatchStore, "store", "s", false, "Store clipboard changes to history (no server required)") clipWatchCmd.Flags().BoolVarP(&clipWatchStore, "store", "s", false, "Store clipboard changes to history (no server required)")
clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration")
clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd) clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd)
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd) clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
} }
func runClipCopy(cmd *cobra.Command, args []string) { func runClipCopy(cmd *cobra.Command, args []string) {
@@ -606,3 +644,154 @@ func runClipConfigSet(cmd *cobra.Command, args []string) {
fmt.Println("Config updated") fmt.Println("Config updated")
} }
func runClipExport(cmd *cobra.Command, args []string) {
req := models.Request{
ID: 1,
Method: "clipboard.getHistory",
}
resp, err := sendServerRequest(req)
if err != nil {
log.Fatalf("Failed to get clipboard history: %v", err)
}
if resp.Error != "" {
log.Fatalf("Error: %s", resp.Error)
}
if resp.Result == nil {
log.Fatal("No clipboard history")
}
out, err := json.MarshalIndent(resp.Result, "", " ")
if err != nil {
log.Fatalf("Failed to marshal: %v", err)
}
if len(args) == 0 {
fmt.Println(string(out))
return
}
if err := os.WriteFile(args[0], out, 0644); err != nil {
log.Fatalf("Failed to write file: %v", err)
}
fmt.Printf("Exported to %s\n", args[0])
}
func runClipImport(cmd *cobra.Command, args []string) {
data, err := os.ReadFile(args[0])
if err != nil {
log.Fatalf("Failed to read file: %v", err)
}
var entries []map[string]any
if err := json.Unmarshal(data, &entries); err != nil {
log.Fatalf("Failed to parse JSON: %v", err)
}
var imported int
for _, entry := range entries {
dataStr, ok := entry["data"].(string)
if !ok {
continue
}
mimeType, _ := entry["mimeType"].(string)
if mimeType == "" {
mimeType = "text/plain"
}
var entryData []byte
if decoded, err := base64.StdEncoding.DecodeString(dataStr); err == nil {
entryData = decoded
} else {
entryData = []byte(dataStr)
}
if err := clipboard.Store(entryData, mimeType); err != nil {
log.Errorf("Failed to store entry: %v", err)
continue
}
imported++
}
fmt.Printf("Imported %d entries\n", imported)
}
func runClipMigrate(cmd *cobra.Command, args []string) {
dbPath := getCliphistPath()
if len(args) > 0 {
dbPath = args[0]
}
if _, err := os.Stat(dbPath); err != nil {
log.Fatalf("Cliphist db not found: %s", dbPath)
}
db, err := bolt.Open(dbPath, 0644, &bolt.Options{
ReadOnly: true,
Timeout: 1 * time.Second,
})
if err != nil {
log.Fatalf("Failed to open cliphist db: %v", err)
}
defer db.Close()
var migrated int
err = db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("b"))
if b == nil {
return fmt.Errorf("cliphist bucket not found")
}
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
if len(v) == 0 {
continue
}
mimeType := detectMimeType(v)
if err := clipboard.Store(v, mimeType); err != nil {
log.Errorf("Failed to store entry %d: %v", btoi(k), err)
continue
}
migrated++
}
return nil
})
if err != nil {
log.Fatalf("Migration failed: %v", err)
}
fmt.Printf("Migrated %d entries from cliphist\n", migrated)
if !clipMigrateDelete {
return
}
db.Close()
if err := os.Remove(dbPath); err != nil {
log.Errorf("Failed to delete cliphist db: %v", err)
return
}
os.Remove(filepath.Dir(dbPath))
fmt.Println("Deleted cliphist db")
}
func getCliphistPath() string {
cacheDir, err := os.UserCacheDir()
if err != nil {
return filepath.Join(os.Getenv("HOME"), ".cache", "cliphist", "db")
}
return filepath.Join(cacheDir, "cliphist", "db")
}
func detectMimeType(data []byte) string {
if _, _, err := image.DecodeConfig(bytes.NewReader(data)); err == nil {
return "image/png"
}
return "text/plain"
}
func btoi(v []byte) uint64 {
return binary.BigEndian.Uint64(v)
}

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

@@ -57,12 +57,14 @@ var keybindsRemoveCmd = &cobra.Command{
} }
func init() { func init() {
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider") keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay") keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked") keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked")
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)
@@ -110,12 +112,21 @@ func initializeProviders() {
} }
} }
func runKeybindsList(_ *cobra.Command, _ []string) { func runKeybindsList(cmd *cobra.Command, _ []string) {
providerList := keybinds.GetDefaultRegistry().List() providerList := keybinds.GetDefaultRegistry().List()
asJSON, _ := cmd.Flags().GetBool("json")
if asJSON {
output, _ := json.Marshal(providerList)
fmt.Fprintln(os.Stdout, string(output))
return
}
if len(providerList) == 0 { if len(providerList) == 0 {
fmt.Fprintln(os.Stdout, "No providers available") fmt.Fprintln(os.Stdout, "No providers available")
return return
} }
fmt.Fprintln(os.Stdout, "Available providers:") fmt.Fprintln(os.Stdout, "Available providers:")
for _, name := range providerList { for _, name := range providerList {
fmt.Fprintf(os.Stdout, " - %s\n", name) fmt.Fprintf(os.Stdout, " - %s\n", name)
@@ -201,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

@@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"time" "time"
@@ -29,9 +30,16 @@ var matugenQueueCmd = &cobra.Command{
Run: runMatugenQueue, Run: runMatugenQueue,
} }
var matugenCheckCmd = &cobra.Command{
Use: "check",
Short: "Check which template apps are detected",
Run: runMatugenCheck,
}
func init() { func init() {
matugenCmd.AddCommand(matugenGenerateCmd) matugenCmd.AddCommand(matugenGenerateCmd)
matugenCmd.AddCommand(matugenQueueCmd) matugenCmd.AddCommand(matugenQueueCmd)
matugenCmd.AddCommand(matugenCheckCmd)
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} { for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
cmd.Flags().String("state-dir", "", "State directory for cache files") cmd.Flags().String("state-dir", "", "State directory for cache files")
@@ -162,3 +170,12 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
log.Fatalf("Timeout waiting for theme generation") log.Fatalf("Timeout waiting for theme generation")
} }
} }
func runMatugenCheck(cmd *cobra.Command, args []string) {
checks := matugen.CheckTemplates(nil)
data, err := json.Marshal(checks)
if err != nil {
log.Fatalf("Failed to marshal check results: %v", err)
}
fmt.Println(string(data))
}

View File

@@ -9,28 +9,28 @@ require (
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.2 github.com/charmbracelet/log v0.4.2
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/godbus/dbus/v5 v5.2.0 github.com/godbus/dbus/v5 v5.2.2
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
github.com/pilebones/go-udev v0.9.1 github.com/pilebones/go-udev v0.9.1
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3
github.com/spf13/cobra v1.10.1 github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
go.etcd.io/bbolt v1.4.3 go.etcd.io/bbolt v1.4.3
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
golang.org/x/image v0.34.0 golang.org/x/image v0.34.0
) )
require ( require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/clipperhouse/displaywidth v0.6.0 // indirect github.com/clipperhouse/displaywidth v0.6.2 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.2 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 // indirect github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect
@@ -38,21 +38,21 @@ require (
github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect github.com/stretchr/objx v0.5.3 // indirect
golang.org/x/crypto v0.45.0 // indirect golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.48.0 // indirect
) )
require ( require (
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.3.3 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.11.2 // indirect github.com/charmbracelet/x/ansi v0.11.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
@@ -66,7 +66,7 @@ require (
github.com/spf13/afero v1.15.0 github.com/spf13/afero v1.15.0
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.38.0 golang.org/x/sys v0.39.0
golang.org/x/text v0.32.0 golang.org/x/text v0.32.0
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@@ -18,6 +18,8 @@ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlv
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI= github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4= github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
@@ -26,18 +28,24 @@ github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsy
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs= github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg= github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s= github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
@@ -58,15 +66,22 @@ github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c= github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c=
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k= github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd h1:Gd/f9cGi/3h1JOPaa6er+CkKUGyGX2DBJdFbDKVO+R0=
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd/go.mod h1:d3XQcsHu1idnquxt48kAv+h+1MUiYKLH/e7LAzjP+pI=
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w= github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU= github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146 h1:xYfxAopYyL44ot6dMBIb1Z1njFM0ZBQ99HdIB99KxLs=
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY= github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs= github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19 h1:0lz2eJScP8v5YZQsrEw+ggWC5jNySjg4bIZo5BIh6iI=
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19/go.mod h1:L+Evfcs7EdTqxwv854354cb6+++7TFL3hJn3Wy4g+3w=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8= github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -114,12 +129,16 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI= github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI=
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28= github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3 h1:msKaIZrrNpvofLPDzNBW3152PJBsnPZsoNNosOCS+C0=
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -133,22 +152,32 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8= golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU= golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -55,7 +55,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
return fmt.Errorf("data too large: %d > %d", len(data), cfg.MaxEntrySize) return fmt.Errorf("data too large: %d > %d", len(data), cfg.MaxEntrySize)
} }
dbPath, err := getDBPath() dbPath, err := GetDBPath()
if err != nil { if err != nil {
return fmt.Errorf("get db path: %w", err) return fmt.Errorf("get db path: %w", err)
} }
@@ -111,7 +111,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
}) })
} }
func getDBPath() (string, error) { func GetDBPath() (string, error) {
cacheDir, err := os.UserCacheDir() cacheDir, err := os.UserCacheDir()
if err != nil { if err != nil {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
@@ -121,12 +121,31 @@ func getDBPath() (string, error) {
cacheDir = filepath.Join(homeDir, ".cache") cacheDir = filepath.Join(homeDir, ".cache")
} }
dbDir := filepath.Join(cacheDir, "dms-clipboard") newDir := filepath.Join(cacheDir, "DankMaterialShell", "clipboard")
if err := os.MkdirAll(dbDir, 0700); err != nil { newPath := filepath.Join(newDir, "db")
return "", err
if _, err := os.Stat(newPath); err == nil {
return newPath, nil
} }
return filepath.Join(dbDir, "db"), nil oldDir := filepath.Join(cacheDir, "dms-clipboard")
oldPath := filepath.Join(oldDir, "db")
if _, err := os.Stat(oldPath); err == nil {
if err := os.MkdirAll(newDir, 0700); err != nil {
return "", err
}
if err := os.Rename(oldPath, newPath); err != nil {
return "", err
}
os.Remove(oldDir)
return newPath, nil
}
if err := os.MkdirAll(newDir, 0700); err != nil {
return "", err
}
return newPath, nil
} }
func deduplicateInTx(b *bolt.Bucket, hash uint64) error { func deduplicateInTx(b *bolt.Bucket, hash uint64) error {

View File

@@ -176,7 +176,7 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
} }
if existingConfig != "" { if existingConfig != "" {
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig) mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig, dmsDir)
if err != nil { if err != nil {
cd.log(fmt.Sprintf("Warning: Failed to merge output sections: %v", err)) cd.log(fmt.Sprintf("Warning: Failed to merge output sections: %v", err))
} else { } else {
@@ -209,6 +209,8 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
{"layout.kdl", NiriLayoutConfig}, {"layout.kdl", NiriLayoutConfig},
{"alttab.kdl", NiriAlttabConfig}, {"alttab.kdl", NiriAlttabConfig},
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)}, {"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
{"outputs.kdl", ""},
{"cursor.kdl", ""},
} }
for _, cfg := range configs { for _, cfg := range configs {
@@ -421,24 +423,31 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
return results, nil return results, nil
} }
// mergeNiriOutputSections extracts output sections from existing config and merges them into the new config func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dmsDir string) (string, error) {
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
// Regular expression to match output sections (including commented ones)
outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`) outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
// Find all output sections in the existing config
existingOutputs := outputRegex.FindAllString(existingConfig, -1) existingOutputs := outputRegex.FindAllString(existingConfig, -1)
if len(existingOutputs) == 0 { if len(existingOutputs) == 0 {
// No output sections to merge
return newConfig, nil return newConfig, nil
} }
// Remove the example output section from the new config outputsPath := filepath.Join(dmsDir, "outputs.kdl")
if _, err := os.Stat(outputsPath); err != nil {
var outputsContent strings.Builder
for _, output := range existingOutputs {
outputsContent.WriteString(output)
outputsContent.WriteString("\n\n")
}
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0644); err != nil {
cd.log(fmt.Sprintf("Warning: Failed to migrate outputs to %s: %v", outputsPath, err))
} else {
cd.log("Migrated output sections to dms/outputs.kdl")
}
}
exampleOutputRegex := regexp.MustCompile(`(?m)^/-output "eDP-2" \{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`) exampleOutputRegex := regexp.MustCompile(`(?m)^/-output "eDP-2" \{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
mergedConfig := exampleOutputRegex.ReplaceAllString(newConfig, "") mergedConfig := exampleOutputRegex.ReplaceAllString(newConfig, "")
// Find where to insert the output sections (after the input section)
inputEndRegex := regexp.MustCompile(`(?m)^}$`) inputEndRegex := regexp.MustCompile(`(?m)^}$`)
inputMatches := inputEndRegex.FindAllStringIndex(newConfig, -1) inputMatches := inputEndRegex.FindAllStringIndex(newConfig, -1)
@@ -446,7 +455,6 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig stri
return "", fmt.Errorf("could not find insertion point for output sections") return "", fmt.Errorf("could not find insertion point for output sections")
} }
// Insert after the first closing brace (end of input section)
insertPos := inputMatches[0][1] insertPos := inputMatches[0][1]
var builder strings.Builder var builder strings.Builder
@@ -476,6 +484,12 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
return result, result.Error return result, result.Error
} }
dmsDir := filepath.Join(configDir, "dms")
if err := os.MkdirAll(dmsDir, 0755); err != nil {
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
return result, result.Error
}
var existingConfig string var existingConfig string
if _, err := os.Stat(result.Path); err == nil { if _, err := os.Stat(result.Path); err == nil {
cd.log("Found existing Hyprland configuration") cd.log("Found existing Hyprland configuration")
@@ -515,7 +529,7 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
} }
if existingConfig != "" { if existingConfig != "" {
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig) mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir)
if err != nil { if err != nil {
cd.log(fmt.Sprintf("Warning: Failed to merge monitor sections: %v", err)) cd.log(fmt.Sprintf("Warning: Failed to merge monitor sections: %v", err))
} else { } else {
@@ -529,13 +543,44 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
return result, result.Error return result, result.Error
} }
if err := cd.deployHyprlandDmsConfigs(dmsDir, terminalCommand); err != nil {
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
return result, result.Error
}
result.Deployed = true result.Deployed = true
cd.log("Successfully deployed Hyprland configuration") cd.log("Successfully deployed Hyprland configuration")
return result, nil return result, nil
} }
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalCommand string) error {
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) { configs := []struct {
name string
content string
}{
{"colors.conf", HyprColorsConfig},
{"layout.conf", HyprLayoutConfig},
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
{"outputs.conf", ""},
{"cursor.conf", ""},
}
for _, cfg := range configs {
path := filepath.Join(dmsDir, cfg.name)
if _, err := os.Stat(path); err == nil {
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
continue
}
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
}
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
}
return nil
}
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir string) (string, error) {
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`) monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
existingMonitors := monitorRegex.FindAllString(existingConfig, -1) existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
@@ -543,6 +588,20 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
return newConfig, nil return newConfig, nil
} }
outputsPath := filepath.Join(dmsDir, "outputs.conf")
if _, err := os.Stat(outputsPath); err != nil {
var outputsContent strings.Builder
for _, monitor := range existingMonitors {
outputsContent.WriteString(monitor)
outputsContent.WriteString("\n")
}
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0644); err != nil {
cd.log(fmt.Sprintf("Warning: Failed to migrate monitors to %s: %v", outputsPath, err))
} else {
cd.log("Migrated monitor sections to dms/outputs.conf")
}
}
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`) exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "") mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")

View File

@@ -161,7 +161,8 @@ layout {
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, err := cd.mergeNiriOutputSections(tt.newConfig, tt.existingConfig) tmpDir := t.TempDir()
result, err := cd.mergeNiriOutputSections(tt.newConfig, tt.existingConfig, tmpDir)
if tt.wantError { if tt.wantError {
assert.Error(t, err) assert.Error(t, err)
@@ -362,7 +363,8 @@ input {
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, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig) tmpDir := t.TempDir()
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig, tmpDir)
if tt.wantError { if tt.wantError {
assert.Error(t, err) assert.Error(t, err)
@@ -406,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 = ")
}) })
@@ -442,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")
}) })
} }
@@ -459,10 +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")
assert.Contains(t, HyprlandConfig, "windowrule = border_size 0, match:class ^(com\\.mitchellh\\.ghostty)$")
} }
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

@@ -0,0 +1,25 @@
# ! Auto-generated file. Do not edit directly.
# Remove source = ./dms/colors.conf from your config to override.
$primary = rgb(d0bcff)
$outline = rgb(948f99)
$error = rgb(f2b8b5)
general {
col.active_border = $primary
col.inactive_border = $outline
}
group {
col.border_active = $primary
col.border_inactive = $outline
col.border_locked_active = $error
col.border_locked_inactive = $outline
groupbar {
col.active = $primary
col.inactive = $outline
col.locked_active = $error
col.locked_inactive = $outline
}
}

View File

@@ -0,0 +1,11 @@
# Auto-generated by DMS - do not edit manually
general {
gaps_in = 4
gaps_out = 4
border_size = 2
}
decoration {
rounding = 12
}

View File

@@ -27,10 +27,7 @@ input {
general { general {
gaps_in = 5 gaps_in = 5
gaps_out = 5 gaps_out = 5
border_size = 0 # off in niri border_size = 2
col.active_border = rgba(707070ff)
col.inactive_border = rgba(d0d0d0ff)
layout = dwindle layout = dwindle
} }
@@ -42,7 +39,7 @@ decoration {
rounding = 12 rounding = 12
active_opacity = 1.0 active_opacity = 1.0
inactive_opacity = 0.9 inactive_opacity = 1.0
shadow { shadow {
enabled = true enabled = true
@@ -93,7 +90,6 @@ misc {
windowrule = tile on, match:class ^(org\.wezfurlong\.wezterm)$ windowrule = tile on, match:class ^(org\.wezfurlong\.wezterm)$
windowrule = rounding 12, match:class ^(org\.gnome\.) windowrule = rounding 12, match:class ^(org\.gnome\.)
windowrule = border_size 0, match:class ^(org\.gnome\.)
windowrule = tile on, match:class ^(gnome-control-center)$ windowrule = tile on, match:class ^(gnome-control-center)$
windowrule = tile on, match:class ^(pavucontrol)$ windowrule = tile on, match:class ^(pavucontrol)$
@@ -106,178 +102,17 @@ windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
windowrule = float on, match:class ^(steam)$ windowrule = float on, match:class ^(steam)$
windowrule = float on, match:class ^(xdg-desktop-portal)$ windowrule = float on, match:class ^(xdg-desktop-portal)$
windowrule = border_size 0, match:class ^(org\.wezfurlong\.wezterm)$
windowrule = border_size 0, match:class ^(Alacritty)$
windowrule = border_size 0, match:class ^(zen)$
windowrule = border_size 0, match:class ^(com\.mitchellh\.ghostty)$
windowrule = border_size 0, match:class ^(kitty)$
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$ 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)$
# ================== source = ./dms/colors.conf
# KEYBINDINGS source = ./dms/outputs.conf
# ================== source = ./dms/layout.conf
$mod = SUPER source = ./dms/cursor.conf
source = ./dms/binds.conf
# === 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

View File

@@ -1,8 +1,3 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
binds { binds {
// === System & Overview === // === System & Overview ===
Mod+D repeat=false { toggle-overview; } Mod+D repeat=false { toggle-overview; }
@@ -20,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

@@ -1,21 +1,19 @@
// ! DO NOT EDIT ! // ! Auto-generated file. Do not edit directly.
// ! AUTO-GENERATED BY DMS ! // Remove `include "dms/colors.kdl"` from your config to override.
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
layout { layout {
background-color "transparent" background-color "transparent"
focus-ring { focus-ring {
active-color "#9dcbfb" active-color "#d0bcff"
inactive-color "#8c9199" inactive-color "#948f99"
urgent-color "#ffb4ab" urgent-color "#f2b8b5"
} }
border { border {
active-color "#9dcbfb" active-color "#d0bcff"
inactive-color "#8c9199" inactive-color "#948f99"
urgent-color "#ffb4ab" urgent-color "#f2b8b5"
} }
shadow { shadow {
@@ -23,19 +21,19 @@ layout {
} }
tab-indicator { tab-indicator {
active-color "#9dcbfb" active-color "#d0bcff"
inactive-color "#8c9199" inactive-color "#948f99"
urgent-color "#ffb4ab" urgent-color "#f2b8b5"
} }
insert-hint { insert-hint {
color "#9dcbfb80" color "#d0bcff80"
} }
} }
recent-windows { recent-windows {
highlight { highlight {
active-color "#124a73" active-color "#4f378b"
urgent-color "#ffb4ab" urgent-color "#f2b8b5"
} }
} }

View File

@@ -240,10 +240,6 @@ window-rule {
match app-id="kitty" match app-id="kitty"
draw-border-with-background false draw-border-with-background false
} }
window-rule {
match is-active=false
opacity 0.9
}
window-rule { window-rule {
match app-id=r#"firefox$"# title="^Picture-in-Picture$" match app-id=r#"firefox$"# title="^Picture-in-Picture$"
match app-id="zoom" match app-id="zoom"
@@ -273,3 +269,5 @@ include "dms/colors.kdl"
include "dms/layout.kdl" include "dms/layout.kdl"
include "dms/alttab.kdl" include "dms/alttab.kdl"
include "dms/binds.kdl" include "dms/binds.kdl"
include "dms/outputs.kdl"
include "dms/cursor.kdl"

View File

@@ -4,3 +4,12 @@ import _ "embed"
//go:embed embedded/hyprland.conf //go:embed embedded/hyprland.conf
var HyprlandConfig string var HyprlandConfig string
//go:embed embedded/hypr-colors.conf
var HyprColorsConfig string
//go:embed embedded/hypr-layout.conf
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)
@@ -461,16 +511,9 @@ func (n *NiriProvider) getBindSortPriority(action string) int {
} }
} }
const dmsWarningHeader = `// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
`
func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string { func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string {
if len(binds) == 0 { if len(binds) == 0 {
return dmsWarningHeader + "binds {}\n" return "binds {}\n"
} }
var regularBinds, recentWindowsBinds []*overrideBind var regularBinds, recentWindowsBinds []*overrideBind
@@ -497,7 +540,6 @@ func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) stri
var sb strings.Builder var sb strings.Builder
sb.WriteString(dmsWarningHeader)
sb.WriteString("binds {\n") sb.WriteString("binds {\n")
for _, bind := range regularBinds { for _, bind := range regularBinds {
n.writeBindNode(&sb, bind, " ") n.writeBindNode(&sb, bind, " ")

View File

@@ -6,13 +6,6 @@ import (
"testing" "testing"
) )
const testHeader = `// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
`
func TestNiriProviderName(t *testing.T) { func TestNiriProviderName(t *testing.T) {
provider := NewNiriProvider("") provider := NewNiriProvider("")
if provider.Name() != "niri" { if provider.Name() != "niri" {
@@ -128,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"},
@@ -204,7 +199,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
{ {
name: "empty binds", name: "empty binds",
binds: map[string]*overrideBind{}, binds: map[string]*overrideBind{},
expected: testHeader + "binds {}\n", expected: "binds {}\n",
}, },
{ {
name: "simple spawn bind", name: "simple spawn bind",
@@ -215,7 +210,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
Description: "Open Terminal", Description: "Open Terminal",
}, },
}, },
expected: testHeader + `binds { expected: `binds {
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; } Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
} }
`, `,
@@ -229,7 +224,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
Description: "Application Launcher", Description: "Application Launcher",
}, },
}, },
expected: testHeader + `binds { expected: `binds {
Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; } Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; }
} }
`, `,
@@ -243,7 +238,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
Options: map[string]any{"allow-when-locked": true}, Options: map[string]any{"allow-when-locked": true},
}, },
}, },
expected: testHeader + `binds { expected: `binds {
XF86AudioMute allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "mute"; } XF86AudioMute allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "mute"; }
} }
`, `,
@@ -257,7 +252,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
Description: "Close Window", Description: "Close Window",
}, },
}, },
expected: testHeader + `binds { expected: `binds {
Mod+Q hotkey-overlay-title="Close Window" { close-window; } Mod+Q hotkey-overlay-title="Close Window" { close-window; }
} }
`, `,
@@ -270,7 +265,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
Action: "next-window", Action: "next-window",
}, },
}, },
expected: testHeader + `binds { expected: `binds {
} }
recent-windows { recent-windows {
@@ -331,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")
@@ -422,7 +469,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
Description: "Focus Workspace 1", Description: "Focus Workspace 1",
}, },
}, },
expected: testHeader + `binds { expected: `binds {
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; } Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
} }
`, `,
@@ -436,7 +483,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
Description: "Focus Workspace 10", Description: "Focus Workspace 10",
}, },
}, },
expected: testHeader + `binds { expected: `binds {
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; } Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
} }
`, `,
@@ -450,7 +497,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
Description: "Adjust Column Width -10%", Description: "Adjust Column Width -10%",
}, },
}, },
expected: testHeader + `binds { expected: `binds {
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; } Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
} }
`, `,
@@ -464,7 +511,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
Description: "Adjust Column Width +10%", Description: "Adjust Column Width +10%",
}, },
}, },
expected: testHeader + `binds { expected: `binds {
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; } Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
} }
`, `,
@@ -493,7 +540,7 @@ func TestNiriGenerateActionWithUnquotedPercentArg(t *testing.T) {
} }
content := provider.generateBindsContent(binds) content := provider.generateBindsContent(binds)
expected := testHeader + `binds { expected := `binds {
Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; } Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; }
} }
` `
@@ -514,7 +561,7 @@ func TestNiriGenerateSpawnWithNumericArgs(t *testing.T) {
} }
content := provider.generateBindsContent(binds) content := provider.generateBindsContent(binds)
expected := testHeader + `binds { expected := `binds {
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; } XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
} }
` `
@@ -535,7 +582,7 @@ func TestNiriGenerateSpawnNumericArgFromCLI(t *testing.T) {
} }
content := provider.generateBindsContent(binds) content := provider.generateBindsContent(binds)
expected := testHeader + `binds { expected := `binds {
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; } XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
} }
` `

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

@@ -23,6 +23,47 @@ const (
ColorModeLight ColorMode = "light" ColorModeLight ColorMode = "light"
) )
type TemplateKind int
const (
TemplateKindNormal TemplateKind = iota
TemplateKindTerminal
TemplateKindGTK
TemplateKindVSCode
)
type TemplateDef struct {
ID string
Commands []string
Flatpaks []string
ConfigFile string
Kind TemplateKind
RunUnconditionally bool
}
var templateRegistry = []TemplateDef{
{ID: "gtk", Kind: TemplateKindGTK, RunUnconditionally: true},
{ID: "niri", Commands: []string{"niri"}, ConfigFile: "niri.toml"},
{ID: "hyprland", Commands: []string{"Hyprland"}, ConfigFile: "hyprland.toml"},
{ID: "mangowc", Commands: []string{"mango"}, ConfigFile: "mangowc.toml"},
{ID: "qt5ct", Commands: []string{"qt5ct"}, ConfigFile: "qt5ct.toml"},
{ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"},
{ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"},
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"},
{ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
{ID: "kitty", Commands: []string{"kitty"}, ConfigFile: "kitty.toml", Kind: TemplateKindTerminal},
{ID: "foot", Commands: []string{"foot"}, ConfigFile: "foot.toml", Kind: TemplateKindTerminal},
{ID: "alacritty", Commands: []string{"alacritty"}, ConfigFile: "alacritty.toml", Kind: TemplateKindTerminal},
{ID: "wezterm", Commands: []string{"wezterm"}, ConfigFile: "wezterm.toml", Kind: TemplateKindTerminal},
{ID: "nvim", Commands: []string{"nvim"}, ConfigFile: "neovim.toml", Kind: TemplateKindTerminal},
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
{ID: "vscode", Kind: TemplateKindVSCode},
}
func (c *ColorMode) GTKTheme() string { func (c *ColorMode) GTKTheme() string {
switch *c { switch *c {
case ColorModeDark: case ColorModeDark:
@@ -240,7 +281,7 @@ func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
if strings.TrimSpace(line) == "[config]" { if strings.TrimSpace(line) == "[config]" {
continue continue
} }
cfgFile.WriteString(substituteShellDir(line, opts.ShellDir) + "\n") cfgFile.WriteString(substituteVars(line, opts.ShellDir) + "\n")
} }
cfgFile.WriteString("\n") cfgFile.WriteString("\n")
} }
@@ -251,73 +292,32 @@ output_path = '%s'
`, opts.ShellDir, opts.ColorsOutput()) `, opts.ShellDir, opts.ColorsOutput())
if !opts.ShouldSkipTemplate("gtk") { homeDir, _ := os.UserHomeDir()
switch opts.Mode { for _, tmpl := range templateRegistry {
case "light": if opts.ShouldSkipTemplate(tmpl.ID) {
appendConfig(opts, cfgFile, nil, nil, "gtk3-light.toml") continue
default:
appendConfig(opts, cfgFile, nil, nil, "gtk3-dark.toml")
} }
}
if !opts.ShouldSkipTemplate("niri") { switch tmpl.Kind {
appendConfig(opts, cfgFile, []string{"niri"}, nil, "niri.toml") case TemplateKindGTK:
} switch opts.Mode {
if !opts.ShouldSkipTemplate("qt5ct") { case ColorModeLight:
appendConfig(opts, cfgFile, []string{"qt5ct"}, nil, "qt5ct.toml") appendConfig(opts, cfgFile, nil, nil, "gtk3-light.toml")
} default:
if !opts.ShouldSkipTemplate("qt6ct") { appendConfig(opts, cfgFile, nil, nil, "gtk3-dark.toml")
appendConfig(opts, cfgFile, []string{"qt6ct"}, nil, "qt6ct.toml") }
} case TemplateKindTerminal:
if !opts.ShouldSkipTemplate("firefox") { appendTerminalConfig(opts, cfgFile, tmpDir, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
appendConfig(opts, cfgFile, []string{"firefox"}, nil, "firefox.toml") case TemplateKindVSCode:
} appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions"), opts.ShellDir)
if !opts.ShouldSkipTemplate("pywalfox") { appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions"), opts.ShellDir)
appendConfig(opts, cfgFile, []string{"pywalfox"}, nil, "pywalfox.toml") appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions"), opts.ShellDir)
} appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
if !opts.ShouldSkipTemplate("zenbrowser") { appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
appendConfig(opts, cfgFile, []string{"zen", "zen-browser"}, []string{"app.zen_browser.zen"}, "zenbrowser.toml") appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
} default:
if !opts.ShouldSkipTemplate("vesktop") { appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
appendConfig(opts, cfgFile, []string{"vesktop"}, []string{"dev.vencord.Vesktop"}, "vesktop.toml") }
}
if !opts.ShouldSkipTemplate("equibop") {
appendConfig(opts, cfgFile, []string{"equibop"}, nil, "equibop.toml")
}
if !opts.ShouldSkipTemplate("ghostty") {
appendTerminalConfig(opts, cfgFile, tmpDir, []string{"ghostty"}, nil, "ghostty.toml")
}
if !opts.ShouldSkipTemplate("kitty") {
appendTerminalConfig(opts, cfgFile, tmpDir, []string{"kitty"}, nil, "kitty.toml")
}
if !opts.ShouldSkipTemplate("foot") {
appendTerminalConfig(opts, cfgFile, tmpDir, []string{"foot"}, nil, "foot.toml")
}
if !opts.ShouldSkipTemplate("alacritty") {
appendTerminalConfig(opts, cfgFile, tmpDir, []string{"alacritty"}, nil, "alacritty.toml")
}
if !opts.ShouldSkipTemplate("wezterm") {
appendTerminalConfig(opts, cfgFile, tmpDir, []string{"wezterm"}, nil, "wezterm.toml")
}
if !opts.ShouldSkipTemplate("nvim") {
appendTerminalConfig(opts, cfgFile, tmpDir, []string{"nvim"}, nil, "neovim.toml")
}
if !opts.ShouldSkipTemplate("dgop") {
appendConfig(opts, cfgFile, []string{"dgop"}, nil, "dgop.toml")
}
if !opts.ShouldSkipTemplate("kcolorscheme") {
appendConfig(opts, cfgFile, nil, nil, "kcolorscheme.toml")
}
if !opts.ShouldSkipTemplate("vscode") {
homeDir, _ := os.UserHomeDir()
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-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, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
} }
if opts.RunUserTemplates { if opts.RunUserTemplates {
@@ -364,7 +364,7 @@ func appendConfig(
if err != nil { if err != nil {
return return
} }
cfgFile.WriteString(substituteShellDir(string(data), opts.ShellDir)) cfgFile.WriteString(substituteVars(string(data), opts.ShellDir))
cfgFile.WriteString("\n") cfgFile.WriteString("\n")
} }
@@ -384,7 +384,7 @@ func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir string, checkC
content := string(data) content := string(data)
if !opts.TerminalsAlwaysDark { if !opts.TerminalsAlwaysDark {
cfgFile.WriteString(substituteShellDir(content, opts.ShellDir)) cfgFile.WriteString(substituteVars(content, opts.ShellDir))
cfgFile.WriteString("\n") cfgFile.WriteString("\n")
return return
} }
@@ -422,7 +422,7 @@ func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir string, checkC
fmt.Sprintf("'%s'", tmpPath)) fmt.Sprintf("'%s'", tmpPath))
} }
cfgFile.WriteString(substituteShellDir(content, opts.ShellDir)) cfgFile.WriteString(substituteVars(content, opts.ShellDir))
cfgFile.WriteString("\n") cfgFile.WriteString("\n")
} }
@@ -467,8 +467,12 @@ output_path = '%s/themes/dankshell-light.json'
log.Infof("Added %s theme config (extension found at %s)", name, extDir) log.Infof("Added %s theme config (extension found at %s)", name, extDir)
} }
func substituteShellDir(content, shellDir string) string { func substituteVars(content, shellDir string) string {
return strings.ReplaceAll(content, "'SHELL_DIR/", "'"+shellDir+"/") result := strings.ReplaceAll(content, "'SHELL_DIR/", "'"+shellDir+"/")
result = strings.ReplaceAll(result, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/")
result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/")
result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/")
return result
} }
func extractTOMLSection(content, startMarker, endMarker string) string { func extractTOMLSection(content, startMarker, endMarker string) string {
@@ -678,3 +682,52 @@ func syncColorScheme(mode ColorMode) {
exec.Command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", "'"+scheme+"'").Run() exec.Command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", "'"+scheme+"'").Run()
} }
} }
type TemplateCheck struct {
ID string `json:"id"`
Detected bool `json:"detected"`
}
func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
if checker == nil {
checker = utils.DefaultAppChecker{}
}
homeDir, _ := os.UserHomeDir()
checks := make([]TemplateCheck, 0, len(templateRegistry))
for _, tmpl := range templateRegistry {
detected := false
switch {
case tmpl.RunUnconditionally:
detected = true
case tmpl.Kind == TemplateKindVSCode:
detected = checkVSCodeExtension(homeDir)
default:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks)
}
checks = append(checks, TemplateCheck{ID: tmpl.ID, Detected: detected})
}
return checks
}
func checkVSCodeExtension(homeDir string) bool {
extDirs := []string{
filepath.Join(homeDir, ".vscode/extensions"),
filepath.Join(homeDir, ".vscode-oss/extensions"),
filepath.Join(homeDir, ".config/Code - OSS/extensions"),
filepath.Join(homeDir, ".cursor/extensions"),
filepath.Join(homeDir, ".windsurf/extensions"),
}
for _, extDir := range extDirs {
pattern := filepath.Join(extDir, "danklinux.dms-theme-*")
if matches, err := filepath.Glob(pattern); err == nil && len(matches) > 0 {
return true
}
}
return false
}

View File

@@ -6,6 +6,8 @@ import (
"testing" "testing"
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils" mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/stretchr/testify/assert"
) )
func TestAppendConfigBinaryExists(t *testing.T) { func TestAppendConfigBinaryExists(t *testing.T) {
@@ -321,3 +323,72 @@ func TestAppendConfigFileDoesNotExist(t *testing.T) {
t.Errorf("expected no config when file doesn't exist, got: %q", string(output)) t.Errorf("expected no config when file doesn't exist, got: %q", string(output))
} }
} }
func TestSubstituteVars(t *testing.T) {
configDir := utils.XDGConfigHome()
dataDir := utils.XDGDataHome()
cacheDir := utils.XDGCacheHome()
tests := []struct {
name string
input string
shellDir string
expected string
}{
{
name: "substitutes SHELL_DIR",
input: "input_path = 'SHELL_DIR/matugen/templates/foo.conf'",
shellDir: "/home/user/shell",
expected: "input_path = '/home/user/shell/matugen/templates/foo.conf'",
},
{
name: "substitutes CONFIG_DIR",
input: "output_path = 'CONFIG_DIR/kitty/theme.conf'",
shellDir: "/home/user/shell",
expected: "output_path = '" + configDir + "/kitty/theme.conf'",
},
{
name: "substitutes DATA_DIR",
input: "output_path = 'DATA_DIR/color-schemes/theme.colors'",
shellDir: "/home/user/shell",
expected: "output_path = '" + dataDir + "/color-schemes/theme.colors'",
},
{
name: "substitutes CACHE_DIR",
input: "output_path = 'CACHE_DIR/wal/colors.json'",
shellDir: "/home/user/shell",
expected: "output_path = '" + cacheDir + "/wal/colors.json'",
},
{
name: "substitutes all dir types",
input: "'SHELL_DIR/a' 'CONFIG_DIR/b' 'DATA_DIR/c' 'CACHE_DIR/d'",
shellDir: "/shell",
expected: "'/shell/a' '" + configDir + "/b' '" + dataDir + "/c' '" + cacheDir + "/d'",
},
{
name: "no substitution when no placeholders",
input: "input_path = '/absolute/path/foo.conf'",
shellDir: "/home/user/shell",
expected: "input_path = '/absolute/path/foo.conf'",
},
{
name: "multiple SHELL_DIR occurrences",
input: "'SHELL_DIR/a' and 'SHELL_DIR/b'",
shellDir: "/shell",
expected: "'/shell/a' and '/shell/b'",
},
{
name: "only substitutes quoted paths",
input: "SHELL_DIR/unquoted and 'SHELL_DIR/quoted'",
shellDir: "/shell",
expected: "SHELL_DIR/unquoted and '/shell/quoted'",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := substituteVars(tc.input, tc.shellDir)
assert.Equal(t, tc.expected, result)
})
}
}

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

@@ -24,20 +24,21 @@ import (
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
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",
} }
func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error) { func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error) {
display := wlCtx.Display() display := wlCtx.Display()
dbPath, err := getDBPath() dbPath, err := clipboardstore.GetDBPath()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get db path: %w", err) return nil, fmt.Errorf("failed to get db path: %w", err)
} }
@@ -102,24 +103,6 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error)
return m, nil return m, nil
} }
func getDBPath() (string, error) {
cacheDir := os.Getenv("XDG_CACHE_HOME")
if cacheDir == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
cacheDir = filepath.Join(homeDir, ".cache")
}
dbDir := filepath.Join(cacheDir, "dms-clipboard")
if err := os.MkdirAll(dbDir, 0700); err != nil {
return "", err
}
return filepath.Join(dbDir, "db"), nil
}
func openDB(path string) (*bolt.DB, error) { func openDB(path string) (*bolt.DB, error) {
db, err := bolt.Open(path, 0644, &bolt.Options{ db, err := bolt.Open(path, 0644, &bolt.Options{
Timeout: 1 * time.Second, Timeout: 1 * time.Second,

View File

@@ -13,6 +13,7 @@ const (
dbusNMPath = "/org/freedesktop/NetworkManager" dbusNMPath = "/org/freedesktop/NetworkManager"
dbusNMInterface = "org.freedesktop.NetworkManager" dbusNMInterface = "org.freedesktop.NetworkManager"
dbusNMDeviceInterface = "org.freedesktop.NetworkManager.Device" dbusNMDeviceInterface = "org.freedesktop.NetworkManager.Device"
dbusNMWiredInterface = "org.freedesktop.NetworkManager.Device.Wired"
dbusNMWirelessInterface = "org.freedesktop.NetworkManager.Device.Wireless" dbusNMWirelessInterface = "org.freedesktop.NetworkManager.Device.Wireless"
dbusNMAccessPointInterface = "org.freedesktop.NetworkManager.AccessPoint" dbusNMAccessPointInterface = "org.freedesktop.NetworkManager.AccessPoint"
dbusPropsInterface = "org.freedesktop.DBus.Properties" dbusPropsInterface = "org.freedesktop.DBus.Properties"

View File

@@ -81,44 +81,24 @@ func (b *NetworkManagerBackend) startSignalPump() error {
return err return err
} }
if b.wifiDevice != nil { for _, info := range b.wifiDevices {
dev := b.wifiDevice.(gonetworkmanager.Device)
if err := conn.AddMatchSignal( if err := conn.AddMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dev.GetPath())), dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())),
dbus.WithMatchInterface(dbusPropsInterface), dbus.WithMatchInterface(dbusPropsInterface),
dbus.WithMatchMember("PropertiesChanged"), dbus.WithMatchMember("PropertiesChanged"),
); err != nil { ); err != nil {
conn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
dbus.WithMatchInterface(dbusPropsInterface),
dbus.WithMatchMember("PropertiesChanged"),
)
conn.RemoveSignal(signals) conn.RemoveSignal(signals)
conn.Close() conn.Close()
return err return err
} }
} }
if b.ethernetDevice != nil { for _, info := range b.ethernetDevices {
dev := b.ethernetDevice.(gonetworkmanager.Device)
if err := conn.AddMatchSignal( if err := conn.AddMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dev.GetPath())), dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())),
dbus.WithMatchInterface(dbusPropsInterface), dbus.WithMatchInterface(dbusPropsInterface),
dbus.WithMatchMember("PropertiesChanged"), dbus.WithMatchMember("PropertiesChanged"),
); err != nil { ); err != nil {
conn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
dbus.WithMatchInterface(dbusPropsInterface),
dbus.WithMatchMember("PropertiesChanged"),
)
if b.wifiDevice != nil {
dev := b.wifiDevice.(gonetworkmanager.Device)
conn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dev.GetPath())),
dbus.WithMatchInterface(dbusPropsInterface),
dbus.WithMatchMember("PropertiesChanged"),
)
}
conn.RemoveSignal(signals) conn.RemoveSignal(signals)
conn.Close() conn.Close()
return err return err
@@ -157,19 +137,17 @@ func (b *NetworkManagerBackend) stopSignalPump() {
dbus.WithMatchMember("PropertiesChanged"), dbus.WithMatchMember("PropertiesChanged"),
) )
if b.wifiDevice != nil { for _, info := range b.wifiDevices {
dev := b.wifiDevice.(gonetworkmanager.Device)
b.dbusConn.RemoveMatchSignal( b.dbusConn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dev.GetPath())), dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())),
dbus.WithMatchInterface(dbusPropsInterface), dbus.WithMatchInterface(dbusPropsInterface),
dbus.WithMatchMember("PropertiesChanged"), dbus.WithMatchMember("PropertiesChanged"),
) )
} }
if b.ethernetDevice != nil { for _, info := range b.ethernetDevices {
dev := b.ethernetDevice.(gonetworkmanager.Device)
b.dbusConn.RemoveMatchSignal( b.dbusConn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dev.GetPath())), dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())),
dbus.WithMatchInterface(dbusPropsInterface), dbus.WithMatchInterface(dbusPropsInterface),
dbus.WithMatchMember("PropertiesChanged"), dbus.WithMatchMember("PropertiesChanged"),
) )
@@ -232,7 +210,10 @@ func (b *NetworkManagerBackend) handleDBusSignal(sig *dbus.Signal) {
b.handleNetworkManagerChange(changes) b.handleNetworkManagerChange(changes)
case dbusNMDeviceInterface: case dbusNMDeviceInterface:
b.handleDeviceChange(changes) b.handleDeviceChange(sig.Path, changes)
case dbusNMWiredInterface:
b.handleWiredChange(changes)
case dbusNMWirelessInterface: case dbusNMWirelessInterface:
b.handleWiFiChange(changes) b.handleWiFiChange(changes)
@@ -278,9 +259,10 @@ func (b *NetworkManagerBackend) handleNetworkManagerChange(changes map[string]db
} }
} }
func (b *NetworkManagerBackend) handleDeviceChange(changes map[string]dbus.Variant) { func (b *NetworkManagerBackend) handleDeviceChange(devicePath dbus.ObjectPath, changes map[string]dbus.Variant) {
var needsUpdate bool var needsUpdate bool
var stateChanged bool var stateChanged bool
var managedChanged bool
for key := range changes { for key := range changes {
switch key { switch key {
@@ -289,21 +271,61 @@ func (b *NetworkManagerBackend) handleDeviceChange(changes map[string]dbus.Varia
needsUpdate = true needsUpdate = true
case "Ip4Config": case "Ip4Config":
needsUpdate = true needsUpdate = true
case "Managed":
managedChanged = true
default: default:
continue continue
} }
} }
if needsUpdate { if managedChanged {
b.updateEthernetState() if managedVariant, ok := changes["Managed"]; ok {
b.updateWiFiState() if managed, ok := managedVariant.Value().(bool); ok && managed {
if stateChanged { b.handleDeviceAdded(devicePath)
b.updatePrimaryConnection() return
}
} }
if b.onStateChange != nil { }
b.onStateChange()
if !needsUpdate {
return
}
b.updateAllEthernetDevices()
b.updateEthernetState()
b.updateAllWiFiDevices()
b.updateWiFiState()
if stateChanged {
b.listEthernetConnections()
b.updatePrimaryConnection()
}
if b.onStateChange != nil {
b.onStateChange()
}
}
func (b *NetworkManagerBackend) handleWiredChange(changes map[string]dbus.Variant) {
var needsUpdate bool
for key := range changes {
switch key {
case "Carrier", "Speed", "HwAddress":
needsUpdate = true
default:
continue
} }
} }
if !needsUpdate {
return
}
b.updateAllEthernetDevices()
b.updateEthernetState()
b.updatePrimaryConnection()
if b.onStateChange != nil {
b.onStateChange()
}
} }
func (b *NetworkManagerBackend) handleWiFiChange(changes map[string]dbus.Variant) { func (b *NetworkManagerBackend) handleWiFiChange(changes map[string]dbus.Variant) {
@@ -369,6 +391,18 @@ func (b *NetworkManagerBackend) handleDeviceAdded(devicePath dbus.ObjectPath) {
return return
} }
if devType != gonetworkmanager.NmDeviceTypeEthernet && devType != gonetworkmanager.NmDeviceTypeWifi {
return
}
if b.dbusConn != nil {
b.dbusConn.AddMatchSignal(
dbus.WithMatchObjectPath(devicePath),
dbus.WithMatchInterface(dbusPropsInterface),
dbus.WithMatchMember("PropertiesChanged"),
)
}
managed, _ := dev.GetPropertyManaged() managed, _ := dev.GetPropertyManaged()
if !managed { if !managed {
return return
@@ -398,14 +432,6 @@ func (b *NetworkManagerBackend) handleDeviceAdded(devicePath dbus.ObjectPath) {
b.ethernetDevice = dev b.ethernetDevice = dev
} }
if b.dbusConn != nil {
b.dbusConn.AddMatchSignal(
dbus.WithMatchObjectPath(devicePath),
dbus.WithMatchInterface(dbusPropsInterface),
dbus.WithMatchMember("PropertiesChanged"),
)
}
b.updateAllEthernetDevices() b.updateAllEthernetDevices()
b.updateEthernetState() b.updateEthernetState()
b.listEthernetConnections() b.listEthernetConnections()
@@ -430,14 +456,6 @@ func (b *NetworkManagerBackend) handleDeviceAdded(devicePath dbus.ObjectPath) {
b.wifiDev = w b.wifiDev = w
} }
if b.dbusConn != nil {
b.dbusConn.AddMatchSignal(
dbus.WithMatchObjectPath(devicePath),
dbus.WithMatchInterface(dbusPropsInterface),
dbus.WithMatchMember("PropertiesChanged"),
)
}
b.updateAllWiFiDevices() b.updateAllWiFiDevices()
b.updateWiFiState() b.updateWiFiState()
} }

View File

@@ -159,7 +159,7 @@ func TestNetworkManagerBackend_HandleDeviceChange(t *testing.T) {
} }
assert.NotPanics(t, func() { assert.NotPanics(t, func() {
backend.handleDeviceChange(changes) backend.handleDeviceChange("/org/freedesktop/NetworkManager/Devices/1", changes)
}) })
} }
@@ -174,7 +174,7 @@ func TestNetworkManagerBackend_HandleDeviceChange_Ip4Config(t *testing.T) {
} }
assert.NotPanics(t, func() { assert.NotPanics(t, func() {
backend.handleDeviceChange(changes) backend.handleDeviceChange("/org/freedesktop/NetworkManager/Devices/1", changes)
}) })
} }

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

@@ -2,9 +2,21 @@ package network
import ( import (
"fmt" "fmt"
"os/exec"
"time" "time"
"github.com/Wifx/gonetworkmanager/v2" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/godbus/dbus/v5"
)
const (
priorityHigh = int32(100)
priorityLow = int32(10)
priorityDefault = int32(0)
metricPreferred = int64(100)
metricNonPreferred = int64(300)
metricDefault = int64(100)
) )
func (m *Manager) SetConnectionPreference(pref ConnectionPreference) error { func (m *Manager) SetConnectionPreference(pref ConnectionPreference) error {
@@ -36,83 +48,124 @@ func (m *Manager) SetConnectionPreference(pref ConnectionPreference) error {
} }
func (m *Manager) prioritizeWiFi() error { func (m *Manager) prioritizeWiFi() error {
if err := m.setConnectionMetrics("802-11-wireless", 50); err != nil { if err := m.setConnectionPriority("802-11-wireless", priorityHigh, metricPreferred); err != nil {
return err log.Warnf("Failed to set WiFi priority: %v", err)
} }
if err := m.setConnectionMetrics("802-3-ethernet", 100); err != nil { if err := m.setConnectionPriority("802-3-ethernet", priorityLow, metricNonPreferred); err != nil {
return err log.Warnf("Failed to set Ethernet priority: %v", err)
} }
m.reapplyActiveConnections()
m.notifySubscribers() m.notifySubscribers()
return nil return nil
} }
func (m *Manager) prioritizeEthernet() error { func (m *Manager) prioritizeEthernet() error {
if err := m.setConnectionMetrics("802-3-ethernet", 50); err != nil { if err := m.setConnectionPriority("802-3-ethernet", priorityHigh, metricPreferred); err != nil {
return err log.Warnf("Failed to set Ethernet priority: %v", err)
} }
if err := m.setConnectionMetrics("802-11-wireless", 100); err != nil { if err := m.setConnectionPriority("802-11-wireless", priorityLow, metricNonPreferred); err != nil {
return err log.Warnf("Failed to set WiFi priority: %v", err)
} }
m.reapplyActiveConnections()
m.notifySubscribers() m.notifySubscribers()
return nil return nil
} }
func (m *Manager) balancePriorities() error { func (m *Manager) balancePriorities() error {
if err := m.setConnectionMetrics("802-3-ethernet", 50); err != nil { if err := m.setConnectionPriority("802-3-ethernet", priorityDefault, metricDefault); err != nil {
return err log.Warnf("Failed to reset Ethernet priority: %v", err)
} }
if err := m.setConnectionMetrics("802-11-wireless", 50); err != nil { if err := m.setConnectionPriority("802-11-wireless", priorityDefault, metricDefault); err != nil {
return err log.Warnf("Failed to reset WiFi priority: %v", err)
} }
m.reapplyActiveConnections()
m.notifySubscribers() m.notifySubscribers()
return nil return nil
} }
func (m *Manager) setConnectionMetrics(connType string, metric uint32) error { func (m *Manager) reapplyActiveConnections() {
settingsMgr, err := gonetworkmanager.NewSettings() m.stateMutex.RLock()
ethDev := m.state.EthernetDevice
wifiDev := m.state.WiFiDevice
m.stateMutex.RUnlock()
if ethDev != "" {
exec.Command("nmcli", "dev", "reapply", ethDev).Run()
}
if wifiDev != "" {
exec.Command("nmcli", "dev", "reapply", wifiDev).Run()
}
}
func (m *Manager) setConnectionPriority(connType string, autoconnectPriority int32, routeMetric int64) error {
conn, err := dbus.ConnectSystemBus()
if err != nil { if err != nil {
return fmt.Errorf("failed to get settings: %w", err) return fmt.Errorf("failed to connect to system bus: %w", err)
}
defer conn.Close()
settingsObj := conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/Settings")
var connPaths []dbus.ObjectPath
if err := settingsObj.Call("org.freedesktop.NetworkManager.Settings.ListConnections", 0).Store(&connPaths); err != nil {
return fmt.Errorf("failed to list connections: %w", err)
} }
connections, err := settingsMgr.ListConnections() for _, connPath := range connPaths {
if err != nil { connObj := conn.Object("org.freedesktop.NetworkManager", connPath)
return fmt.Errorf("failed to get connections: %w", err)
}
for _, conn := range connections { var settings map[string]map[string]dbus.Variant
connSettings, err := conn.GetSettings() if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&settings); err != nil {
if err != nil {
continue continue
} }
if connMeta, ok := connSettings["connection"]; ok { connSection, ok := settings["connection"]
if cType, ok := connMeta["type"].(string); ok && cType == connType { if !ok {
if connSettings["ipv4"] == nil { continue
connSettings["ipv4"] = make(map[string]any)
}
if ipv4Map := connSettings["ipv4"]; ipv4Map != nil {
ipv4Map["route-metric"] = int64(metric)
}
if connSettings["ipv6"] == nil {
connSettings["ipv6"] = make(map[string]any)
}
if ipv6Map := connSettings["ipv6"]; ipv6Map != nil {
ipv6Map["route-metric"] = int64(metric)
}
err = conn.Update(connSettings)
if err != nil {
continue
}
}
} }
typeVariant, ok := connSection["type"]
if !ok {
continue
}
cType, ok := typeVariant.Value().(string)
if !ok || cType != connType {
continue
}
connName := ""
if idVariant, ok := connSection["id"]; ok {
connName, _ = idVariant.Value().(string)
}
if connName == "" {
continue
}
if err := exec.Command("nmcli", "con", "mod", connName,
"connection.autoconnect-priority", fmt.Sprintf("%d", autoconnectPriority)).Run(); err != nil {
log.Warnf("Failed to set autoconnect-priority for %v: %v", connName, err)
continue
}
if err := exec.Command("nmcli", "con", "mod", connName,
"ipv4.route-metric", fmt.Sprintf("%d", routeMetric)).Run(); err != nil {
log.Warnf("Failed to set ipv4.route-metric for %v: %v", connName, err)
}
if err := exec.Command("nmcli", "con", "mod", connName,
"ipv6.route-metric", fmt.Sprintf("%d", routeMetric)).Run(); err != nil {
log.Warnf("Failed to set ipv6.route-metric for %v: %v", connName, err)
}
log.Infof("Updated %v: autoconnect-priority=%d, route-metric=%d", connName, autoconnectPriority, routeMetric)
} }
return nil return nil
@@ -125,14 +178,18 @@ func (m *Manager) GetConnectionPreference() ConnectionPreference {
} }
func (m *Manager) WasRecentlyFailed(ssid string) bool { func (m *Manager) WasRecentlyFailed(ssid string) bool {
if nm, ok := m.backend.(*NetworkManagerBackend); ok { nm, ok := m.backend.(*NetworkManagerBackend)
nm.failedMutex.RLock() if !ok {
defer nm.failedMutex.RUnlock() return false
if nm.lastFailedSSID == ssid {
elapsed := time.Now().Unix() - nm.lastFailedTime
return elapsed < 10
}
} }
return false
nm.failedMutex.RLock()
defer nm.failedMutex.RUnlock()
if nm.lastFailedSSID != ssid {
return false
}
elapsed := time.Now().Unix() - nm.lastFailedTime
return elapsed < 10
} }

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

@@ -10,9 +10,32 @@ func XDGStateHome() string {
if dir := os.Getenv("XDG_STATE_HOME"); dir != "" { if dir := os.Getenv("XDG_STATE_HOME"); dir != "" {
return dir return dir
} }
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
return filepath.Join(append([]string{home}, ".local", "state")...) return filepath.Join(home, ".local", "state")
}
func XDGDataHome() string {
if dir := os.Getenv("XDG_DATA_HOME"); dir != "" {
return dir
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".local", "share")
}
func XDGCacheHome() string {
if dir, err := os.UserCacheDir(); err == nil {
return dir
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".cache")
}
func XDGConfigHome() string {
if dir, err := os.UserConfigDir(); err == nil {
return dir
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".config")
} }
func ExpandPath(path string) (string, error) { func ExpandPath(path string) (string, error) {

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 (
@@ -76,14 +78,14 @@
inherit version; inherit version;
pname = "dms-shell"; pname = "dms-shell";
src = ./core; src = ./core;
vendorHash = "sha256-DINaA5LCOWoxBIewuc39Rnwj6NdZoET7Q++B11Qg5rI="; vendorHash = "sha256-9CnZFtjXXWYELRiBX2UbZvWopnl9Y1ILuK+xP6YQZ9U=";
subPackages = [ "cmd/dms" ]; subPackages = [ "cmd/dms" ];
ldflags = [ ldflags = [
"-s" "-s"
"-w" "-w"
"-X main.Version=${version}" "-X 'main.Version=${version}'"
]; ];
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [

View File

@@ -7,120 +7,109 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
Singleton { Singleton {
id: root id: root
property var appUsageRanking: { property var appUsageRanking: {}
}
Component.onCompleted: { Component.onCompleted: {
loadSettings() loadSettings();
} }
function loadSettings() { function loadSettings() {
parseSettings(settingsFile.text()) parseSettings(settingsFile.text());
} }
function parseSettings(content) { function parseSettings(content) {
try { try {
if (content && content.trim()) { if (content && content.trim()) {
var settings = JSON.parse(content) var settings = JSON.parse(content);
appUsageRanking = settings.appUsageRanking || {} appUsageRanking = settings.appUsageRanking || {};
} }
} catch (e) { } catch (e) {}
}
} }
function saveSettings() { function saveSettings() {
settingsFile.setText(JSON.stringify({ settingsFile.setText(JSON.stringify({
"appUsageRanking": appUsageRanking "appUsageRanking": appUsageRanking
}, null, 2)) }, null, 2));
} }
function addAppUsage(app) { function addAppUsage(app) {
if (!app) if (!app)
return return;
var appId = app.id || (app.execString || app.exec || "");
var appId = app.id || (app.execString || app.exec || "")
if (!appId) if (!appId)
return return;
var currentRanking = Object.assign({}, appUsageRanking);
var currentRanking = Object.assign({}, appUsageRanking)
if (currentRanking[appId]) { if (currentRanking[appId]) {
currentRanking[appId].usageCount = (currentRanking[appId].usageCount currentRanking[appId].usageCount = (currentRanking[appId].usageCount || 1) + 1;
|| 1) + 1 currentRanking[appId].lastUsed = Date.now();
currentRanking[appId].lastUsed = Date.now() currentRanking[appId].icon = app.icon ? String(app.icon) : (currentRanking[appId].icon || "application-x-executable");
currentRanking[appId].icon = app.icon || currentRanking[appId].icon currentRanking[appId].name = app.name || currentRanking[appId].name || "";
|| "application-x-executable"
currentRanking[appId].name = app.name
|| currentRanking[appId].name || ""
} else { } else {
currentRanking[appId] = { currentRanking[appId] = {
"name": app.name || "", "name": app.name || "",
"exec": app.execString || app.exec || "", "exec": app.execString || app.exec || "",
"icon": app.icon || "application-x-executable", "icon": app.icon ? String(app.icon) : "application-x-executable",
"comment": app.comment || "", "comment": app.comment || "",
"usageCount": 1, "usageCount": 1,
"lastUsed": Date.now() "lastUsed": Date.now()
} };
} }
appUsageRanking = currentRanking appUsageRanking = currentRanking;
saveSettings() saveSettings();
} }
function getRankedApps() { function getRankedApps() {
var apps = [] var apps = [];
for (var appId in appUsageRanking) { for (var appId in appUsageRanking) {
var appData = appUsageRanking[appId] var appData = appUsageRanking[appId];
apps.push({ apps.push({
"id": appId, "id": appId,
"name": appData.name, "name": appData.name,
"exec": appData.exec, "exec": appData.exec,
"icon": appData.icon, "icon": appData.icon,
"comment": appData.comment, "comment": appData.comment,
"usageCount": appData.usageCount, "usageCount": appData.usageCount,
"lastUsed": appData.lastUsed "lastUsed": appData.lastUsed
}) });
} }
return apps.sort(function (a, b) { return apps.sort(function (a, b) {
if (a.usageCount !== b.usageCount) if (a.usageCount !== b.usageCount)
return b.usageCount - a.usageCount return b.usageCount - a.usageCount;
return a.name.localeCompare(b.name) return a.name.localeCompare(b.name);
}) });
} }
function cleanupAppUsageRanking(availableAppIds) { function cleanupAppUsageRanking(availableAppIds) {
var currentRanking = Object.assign({}, appUsageRanking) var currentRanking = Object.assign({}, appUsageRanking);
var hasChanges = false var hasChanges = false;
for (var appId in currentRanking) { for (var appId in currentRanking) {
if (availableAppIds.indexOf(appId) === -1) { if (availableAppIds.indexOf(appId) === -1) {
delete currentRanking[appId] delete currentRanking[appId];
hasChanges = true hasChanges = true;
} }
} }
if (hasChanges) { if (hasChanges) {
appUsageRanking = currentRanking appUsageRanking = currentRanking;
saveSettings() saveSettings();
} }
} }
FileView { FileView {
id: settingsFile id: settingsFile
path: StandardPaths.writableLocation( path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/appusage.json"
StandardPaths.GenericStateLocation) + "/DankMaterialShell/appusage.json"
blockLoading: true blockLoading: true
blockWrites: true blockWrites: true
watchChanges: true watchChanges: true
onLoaded: { onLoaded: {
parseSettings(settingsFile.text()) parseSettings(settingsFile.text());
} }
onLoadFailed: error => {} onLoadFailed: error => {}
} }

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,15 +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;
return "transmission"; } else if (sub.type === "regex") {
const match = appId.match(new RegExp(sub.pattern));
if (match) {
return sub.replacement.replace(/\$(\d+)/g, (_, n) => match[n] || "");
}
}
}
const steamMatch = appId.match(/^steam_app_(\d+)$/);
if (steamMatch)
return `steam_icon_${steamMatch[1]}`;
return appId; return appId;
} }
@@ -63,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

@@ -118,8 +118,56 @@ Singleton {
parseSettings(greeterSessionFile.text()); parseSettings(greeterSessionFile.text());
return; return;
} }
parseSettings(settingsFile.text());
_checkSessionWritable(); try {
const txt = settingsFile.text();
let obj = (txt && txt.trim()) ? JSON.parse(txt) : null;
if (obj?.brightnessLogarithmicDevices && !obj?.brightnessExponentialDevices)
obj.brightnessExponentialDevices = obj.brightnessLogarithmicDevices;
if (obj?.nightModeStartTime !== undefined) {
const parts = obj.nightModeStartTime.split(":");
obj.nightModeStartHour = parseInt(parts[0]) || 18;
obj.nightModeStartMinute = parseInt(parts[1]) || 0;
}
if (obj?.nightModeEndTime !== undefined) {
const parts = obj.nightModeEndTime.split(":");
obj.nightModeEndHour = parseInt(parts[0]) || 6;
obj.nightModeEndMinute = parseInt(parts[1]) || 0;
}
const oldVersion = obj?.configVersion ?? 0;
if (obj && oldVersion === 0)
migrateFromUndefinedToV1(obj);
if (obj && oldVersion < sessionConfigVersion) {
const settingsDataRef = (typeof SettingsData !== "undefined") ? SettingsData : null;
const migrated = Store.migrateToVersion(obj, sessionConfigVersion, settingsDataRef);
if (migrated) {
_pendingMigration = migrated;
obj = migrated;
}
}
Store.parse(root, obj);
_loadedSessionSnapshot = getCurrentSessionJson();
_hasLoaded = true;
if (!isGreeterMode && typeof Theme !== "undefined")
Theme.generateSystemThemesFromCurrentTheme();
if (typeof WallpaperCyclingService !== "undefined")
WallpaperCyclingService.updateCyclingState();
_checkSessionWritable();
} catch (e) {
_parseError = true;
const msg = e.message;
console.error("SessionData: Failed to parse session.json - file will not be overwritten. Error:", msg);
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
}
} }
function _checkSessionWritable() { function _checkSessionWritable() {
@@ -158,34 +206,27 @@ Singleton {
function parseSettings(content) { function parseSettings(content) {
_parseError = false; _parseError = false;
try { try {
if (!content || !content.trim()) { let obj = (content && content.trim()) ? JSON.parse(content) : null;
_parseError = true;
return;
}
let obj = JSON.parse(content); if (obj?.brightnessLogarithmicDevices && !obj?.brightnessExponentialDevices)
if (obj.brightnessLogarithmicDevices && !obj.brightnessExponentialDevices) {
obj.brightnessExponentialDevices = obj.brightnessLogarithmicDevices; obj.brightnessExponentialDevices = obj.brightnessLogarithmicDevices;
}
if (obj.nightModeStartTime !== undefined) { if (obj?.nightModeStartTime !== undefined) {
const parts = obj.nightModeStartTime.split(":"); const parts = obj.nightModeStartTime.split(":");
obj.nightModeStartHour = parseInt(parts[0]) || 18; obj.nightModeStartHour = parseInt(parts[0]) || 18;
obj.nightModeStartMinute = parseInt(parts[1]) || 0; obj.nightModeStartMinute = parseInt(parts[1]) || 0;
} }
if (obj.nightModeEndTime !== undefined) { if (obj?.nightModeEndTime !== undefined) {
const parts = obj.nightModeEndTime.split(":"); const parts = obj.nightModeEndTime.split(":");
obj.nightModeEndHour = parseInt(parts[0]) || 6; obj.nightModeEndHour = parseInt(parts[0]) || 6;
obj.nightModeEndMinute = parseInt(parts[1]) || 0; obj.nightModeEndMinute = parseInt(parts[1]) || 0;
} }
const oldVersion = obj.configVersion ?? 0; const oldVersion = obj?.configVersion ?? 0;
if (oldVersion === 0) { if (obj && oldVersion === 0)
migrateFromUndefinedToV1(obj); migrateFromUndefinedToV1(obj);
}
if (oldVersion < sessionConfigVersion) { if (obj && oldVersion < sessionConfigVersion) {
const settingsDataRef = (typeof SettingsData !== "undefined") ? SettingsData : null; const settingsDataRef = (typeof SettingsData !== "undefined") ? SettingsData : null;
const migrated = Store.migrateToVersion(obj, sessionConfigVersion, settingsDataRef); const migrated = Store.migrateToVersion(obj, sessionConfigVersion, settingsDataRef);
if (migrated) { if (migrated) {
@@ -196,22 +237,14 @@ Singleton {
Store.parse(root, obj); Store.parse(root, obj);
if (wallpaperPath && wallpaperPath.startsWith("we:")) {
console.warn("WallpaperEngine wallpaper detected, resetting wallpaper");
wallpaperPath = "";
Quickshell.execDetached(["notify-send", "-u", "critical", "-a", "DMS", "-i", "dialog-warning", "WallpaperEngine Support Moved", "WallpaperEngine support has been moved to a plugin. Please enable the Linux Wallpaper Engine plugin in Settings → Plugins to continue using WallpaperEngine."]);
}
_hasLoaded = true;
_loadedSessionSnapshot = getCurrentSessionJson(); _loadedSessionSnapshot = getCurrentSessionJson();
_hasLoaded = true;
if (!isGreeterMode && typeof Theme !== "undefined") { if (!isGreeterMode && typeof Theme !== "undefined")
Theme.generateSystemThemesFromCurrentTheme(); Theme.generateSystemThemesFromCurrentTheme();
}
if (typeof WallpaperCyclingService !== "undefined") { if (typeof WallpaperCyclingService !== "undefined")
WallpaperCyclingService.updateCyclingState(); WallpaperCyclingService.updateCyclingState();
}
} catch (e) { } catch (e) {
_parseError = true; _parseError = true;
const msg = e.message; const msg = e.message;
@@ -950,8 +983,9 @@ Singleton {
id: settingsFile id: settingsFile
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json" path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
blockLoading: isGreeterMode blockLoading: true
blockWrites: true blockWrites: true
atomicWrites: true
watchChanges: !isGreeterMode watchChanges: !isGreeterMode
onLoaded: { onLoaded: {
if (!isGreeterMode) { if (!isGreeterMode) {

View File

@@ -1,5 +1,5 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior pragma ComponentBehavior: Bound
import QtCore import QtCore
import QtQuick import QtQuick
@@ -62,6 +62,22 @@ Singleton {
property bool _hasUnsavedChanges: false property bool _hasUnsavedChanges: false
property var _loadedSettingsSnapshot: null property var _loadedSettingsSnapshot: null
property var pluginSettings: ({}) property var pluginSettings: ({})
property var builtInPluginSettings: ({})
function getBuiltInPluginSetting(pluginId, key, defaultValue) {
if (!builtInPluginSettings[pluginId])
return defaultValue;
return builtInPluginSettings[pluginId][key] !== undefined ? builtInPluginSettings[pluginId][key] : defaultValue;
}
function setBuiltInPluginSetting(pluginId, key, value) {
const updated = JSON.parse(JSON.stringify(builtInPluginSettings));
if (!updated[pluginId])
updated[pluginId] = {};
updated[pluginId][key] = value;
builtInPluginSettings = updated;
saveSettings();
}
property alias dankBarLeftWidgetsModel: leftWidgetsModel property alias dankBarLeftWidgetsModel: leftWidgetsModel
property alias dankBarCenterWidgetsModel: centerWidgetsModel property alias dankBarCenterWidgetsModel: centerWidgetsModel
@@ -81,6 +97,13 @@ Singleton {
property real cornerRadius: 12 property real cornerRadius: 12
property int niriLayoutGapsOverride: -1 property int niriLayoutGapsOverride: -1
property int niriLayoutRadiusOverride: -1 property int niriLayoutRadiusOverride: -1
property int niriLayoutBorderSize: -1
property int hyprlandLayoutGapsOverride: -1
property int hyprlandLayoutRadiusOverride: -1
property int hyprlandLayoutBorderSize: -1
property int mangoLayoutGapsOverride: -1
property int mangoLayoutRadiusOverride: -1
property int mangoLayoutBorderSize: -1
property bool use24HourClock: true property bool use24HourClock: true
property bool showSeconds: false property bool showSeconds: false
@@ -122,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
@@ -177,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
@@ -192,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: ""
@@ -223,6 +254,25 @@ Singleton {
property bool qt6ctAvailable: false property bool qt6ctAvailable: false
property bool gtkAvailable: false property bool gtkAvailable: false
property var cursorSettings: ({
"theme": "System Default",
"size": 24,
"niri": {
"hideWhenTyping": false,
"hideAfterInactiveMs": 0
},
"hyprland": {
"hideOnKeyPress": false,
"hideOnTouch": false,
"inactiveTimeout": 0
},
"dwl": {
"cursorHideTimeout": 0
}
})
property var availableCursorThemes: ["System Default"]
property string systemDefaultCursorTheme: ""
property string launcherLogoMode: "apps" property string launcherLogoMode: "apps"
property string launcherLogoCustomPath: "" property string launcherLogoCustomPath: ""
property string launcherLogoColorOverride: "" property string launcherLogoColorOverride: ""
@@ -276,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: ({})
@@ -295,6 +345,8 @@ Singleton {
property bool runDmsMatugenTemplates: true property bool runDmsMatugenTemplates: true
property bool matugenTemplateGtk: true property bool matugenTemplateGtk: true
property bool matugenTemplateNiri: true property bool matugenTemplateNiri: true
property bool matugenTemplateHyprland: true
property bool matugenTemplateMangowc: true
property bool matugenTemplateQt5ct: true property bool matugenTemplateQt5ct: true
property bool matugenTemplateQt6ct: true property bool matugenTemplateQt6ct: true
property bool matugenTemplateFirefox: true property bool matugenTemplateFirefox: true
@@ -347,11 +399,13 @@ Singleton {
property bool fprintdAvailable: false property bool fprintdAvailable: false
property string lockScreenActiveMonitor: "all" property string lockScreenActiveMonitor: "all"
property string lockScreenInactiveColor: "#000000" property string lockScreenInactiveColor: "#000000"
property int lockScreenNotificationMode: 0
property bool hideBrightnessSlider: false property bool hideBrightnessSlider: false
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
@@ -434,7 +488,11 @@ Singleton {
"maximizeDetection": true, "maximizeDetection": true,
"scrollEnabled": true, "scrollEnabled": true,
"scrollXBehavior": "column", "scrollXBehavior": "column",
"scrollYBehavior": "workspace" "scrollYBehavior": "workspace",
"shadowIntensity": 0,
"shadowOpacity": 60,
"shadowColorMode": "text",
"shadowCustomColor": "#000000"
} }
] ]
@@ -481,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];
@@ -632,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)
@@ -664,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
@@ -699,10 +894,15 @@ Singleton {
} }
} }
function updateNiriLayout() { function updateCompositorLayout() {
if (typeof NiriService !== "undefined" && typeof CompositorService !== "undefined" && CompositorService.isNiri) { if (typeof CompositorService === "undefined")
return;
if (CompositorService.isNiri && typeof NiriService !== "undefined")
NiriService.generateNiriLayoutConfig(); NiriService.generateNiriLayoutConfig();
} if (CompositorService.isHyprland && typeof HyprlandService !== "undefined")
HyprlandService.generateLayoutConfig();
if (CompositorService.isDwl && typeof DwlService !== "undefined")
DwlService.generateLayoutConfig();
} }
function applyStoredIconTheme() { function applyStoredIconTheme() {
@@ -778,9 +978,10 @@ Singleton {
readonly property var _hooks: ({ readonly property var _hooks: ({
"applyStoredTheme": applyStoredTheme, "applyStoredTheme": applyStoredTheme,
"regenSystemThemes": regenSystemThemes, "regenSystemThemes": regenSystemThemes,
"updateNiriLayout": updateNiriLayout, "updateCompositorLayout": updateCompositorLayout,
"applyStoredIconTheme": applyStoredIconTheme, "applyStoredIconTheme": applyStoredIconTheme,
"updateBarConfigs": updateBarConfigs "updateBarConfigs": updateBarConfigs,
"updateCompositorCursor": updateCompositorCursor
}) })
function set(key, value) { function set(key, value) {
@@ -817,6 +1018,7 @@ Singleton {
_hasLoaded = true; _hasLoaded = true;
applyStoredTheme(); applyStoredTheme();
applyStoredIconTheme(); applyStoredIconTheme();
updateCompositorCursor();
Processes.detectQtTools(); Processes.detectQtTools();
_checkSettingsWritable(); _checkSettingsWritable();
@@ -947,6 +1149,46 @@ Singleton {
}); });
} }
function detectAvailableCursorThemes() {
const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS") || "";
const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation));
const homeDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation));
const dataDirs = xdgDataDirs.trim() !== "" ? xdgDataDirs.split(":").concat([localData]) : ["/usr/share", "/usr/local/share", localData];
const cursorPaths = dataDirs.map(d => d + "/icons").concat([homeDir + "/.icons", homeDir + "/.local/share/icons"]);
const pathsArg = cursorPaths.join(" ");
const script = `
echo "SYSDEFAULT:$(gsettings get org.gnome.desktop.interface cursor-theme 2>/dev/null | sed "s/'//g" || echo '')"
for dir in ${pathsArg}; do
[ -d "$dir" ] || continue
for theme in "$dir"/*/; do
[ -d "$theme" ] || continue
[ -d "$theme/cursors" ] || continue
basename "$theme"
done
done | grep -v '^icons$' | grep -v '^default$' | sort -u
`;
Proc.runCommand("detectCursorThemes", ["sh", "-c", script], (output, exitCode) => {
const themes = ["System Default"];
if (output && output.trim()) {
const lines = output.trim().split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith("SYSDEFAULT:")) {
systemDefaultCursorTheme = line.substring(11).trim();
continue;
}
if (line)
themes.push(line);
}
}
availableCursorThemes = themes;
});
}
function getEffectiveTimeFormat() { function getEffectiveTimeFormat() {
if (use24HourClock) { if (use24HourClock) {
return showSeconds ? "hh:mm:ss" : "hh:mm"; return showSeconds ? "hh:mm:ss" : "hh:mm";
@@ -1459,7 +1701,7 @@ Singleton {
function setCornerRadius(radius) { function setCornerRadius(radius) {
set("cornerRadius", radius); set("cornerRadius", radius);
NiriService.generateNiriLayoutConfig(); updateCompositorLayout();
} }
function setWeatherLocation(displayName, coordinates) { function setWeatherLocation(displayName, coordinates) {
@@ -1475,6 +1717,94 @@ Singleton {
Theme.generateSystemThemesFromCurrentTheme(); Theme.generateSystemThemesFromCurrentTheme();
} }
function setCursorTheme(themeName) {
const updated = JSON.parse(JSON.stringify(cursorSettings));
updated.theme = themeName;
cursorSettings = updated;
saveSettings();
updateCompositorCursor();
}
function setCursorSize(size) {
const updated = JSON.parse(JSON.stringify(cursorSettings));
updated.size = size;
cursorSettings = updated;
saveSettings();
updateCompositorCursor();
}
// This solution for xwayland cursor themes is from the xwls discussion:
// https://github.com/Supreeeme/xwayland-satellite/issues/104
// no idea if this matters on other compositors but we also set XCURSOR stuff in the launcher
function updateCompositorCursor() {
updateXResources();
if (typeof CompositorService === "undefined")
return;
if (CompositorService.isNiri && typeof NiriService !== "undefined") {
NiriService.generateNiriCursorConfig();
return;
}
if (CompositorService.isHyprland && typeof HyprlandService !== "undefined") {
HyprlandService.generateCursorConfig();
return;
}
if (CompositorService.isDwl && typeof DwlService !== "undefined") {
DwlService.generateCursorConfig();
return;
}
}
function updateXResources() {
const homeDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation));
const xresourcesPath = homeDir + "/.Xresources";
const themeName = cursorSettings.theme === "System Default" ? systemDefaultCursorTheme : cursorSettings.theme;
const size = cursorSettings.size || 24;
if (!themeName)
return;
const script = `
xresources_file="${xresourcesPath}"
temp_file="\${xresources_file}.tmp.$$"
theme_name="${themeName}"
cursor_size="${size}"
if [ -f "$xresources_file" ]; then
grep -v '^[[:space:]]*Xcursor\\.theme:' "$xresources_file" | grep -v '^[[:space:]]*Xcursor\\.size:' > "$temp_file" 2>/dev/null || true
else
touch "$temp_file"
fi
echo "Xcursor.theme: $theme_name" >> "$temp_file"
echo "Xcursor.size: $cursor_size" >> "$temp_file"
mv "$temp_file" "$xresources_file"
xrdb -merge "$xresources_file" 2>/dev/null || true
`;
Quickshell.execDetached(["sh", "-c", script]);
}
function getCursorEnvironment() {
const isSystemDefault = cursorSettings.theme === "System Default";
const isDefaultSize = !cursorSettings.size || cursorSettings.size === 24;
if (isSystemDefault && isDefaultSize)
return {};
const themeName = isSystemDefault ? "" : cursorSettings.theme;
const size = String(cursorSettings.size || 24);
const env = {};
if (!isDefaultSize) {
env["XCURSOR_SIZE"] = size;
env["HYPRCURSOR_SIZE"] = size;
}
if (themeName) {
env["XCURSOR_THEME"] = themeName;
env["HYPRCURSOR_THEME"] = themeName;
}
return env;
}
function setGtkThemingEnabled(enabled) { function setGtkThemingEnabled(enabled) {
set("gtkThemingEnabled", enabled); set("gtkThemingEnabled", enabled);
if (enabled && typeof Theme !== "undefined") { if (enabled && typeof Theme !== "undefined") {
@@ -1541,9 +1871,7 @@ Singleton {
"spacing": spacing "spacing": spacing
}); });
} }
if (typeof NiriService !== "undefined" && CompositorService.isNiri) { updateCompositorLayout();
NiriService.generateNiriLayoutConfig();
}
} }
function setDankBarPosition(position) { function setDankBarPosition(position) {
@@ -1654,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")
@@ -1881,6 +2251,7 @@ Singleton {
_hasLoaded = true; _hasLoaded = true;
applyStoredTheme(); applyStoredTheme();
applyStoredIconTheme(); applyStoredIconTheme();
updateCompositorCursor();
} catch (e) { } catch (e) {
_parseError = true; _parseError = true;
const msg = e.message; const msg = e.message;

View File

@@ -89,6 +89,8 @@ Singleton {
property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false
property var workerRunning: false property var workerRunning: false
property var pendingThemeRequest: null property var pendingThemeRequest: null
signal matugenCompleted(string mode, string result)
property var matugenColors: ({}) property var matugenColors: ({})
property var _pendingGenerateParams: null property var _pendingGenerateParams: null
@@ -544,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);
} }
@@ -902,12 +904,16 @@ 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");
if (!SettingsData.matugenTemplateNiri) if (!SettingsData.matugenTemplateNiri)
skipTemplates.push("niri"); skipTemplates.push("niri");
if (!SettingsData.matugenTemplateHyprland)
skipTemplates.push("hyprland");
if (!SettingsData.matugenTemplateMangowc)
skipTemplates.push("mangowc");
if (!SettingsData.matugenTemplateQt5ct) if (!SettingsData.matugenTemplateQt5ct)
skipTemplates.push("qt5ct"); skipTemplates.push("qt5ct");
if (!SettingsData.matugenTemplateQt6ct) if (!SettingsData.matugenTemplateQt6ct)
@@ -1272,24 +1278,32 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
workerRunning = false; workerRunning = false;
const currentMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark";
if (exitCode === 0) { switch (exitCode) {
case 0:
console.info("Theme: Matugen worker completed successfully"); console.info("Theme: Matugen worker completed successfully");
} else if (exitCode === 2) { root.matugenCompleted(currentMode, "success");
break;
case 2:
console.log("Theme: Matugen worker completed with code 2 (no changes needed)"); console.log("Theme: Matugen worker completed with code 2 (no changes needed)");
} else { root.matugenCompleted(currentMode, "no-changes");
break;
default:
if (typeof ToastService !== "undefined") { if (typeof ToastService !== "undefined") {
ToastService.showError("Theme worker failed (" + exitCode + ")"); ToastService.showError("Theme worker failed (" + exitCode + ")");
} }
console.warn("Theme: Matugen worker failed with exit code:", exitCode); console.warn("Theme: Matugen worker failed with exit code:", exitCode);
root.matugenCompleted(currentMode, "error");
} }
if (pendingThemeRequest) { if (!pendingThemeRequest)
const req = pendingThemeRequest; return;
pendingThemeRequest = null;
console.info("Theme: Processing queued theme request"); const req = pendingThemeRequest;
setDesiredTheme(req.kind, req.value, req.isLight, req.iconTheme, req.matugenType, req.stockColors); pendingThemeRequest = null;
} console.info("Theme: Processing queued theme request");
setDesiredTheme(req.kind, req.value, req.isLight, req.iconTheme, req.matugenType, req.stockColors);
} }
} }

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

@@ -19,9 +19,16 @@ var SPEC = {
widgetBackgroundColor: { def: "sch" }, widgetBackgroundColor: { def: "sch" },
widgetColorMode: { def: "default" }, widgetColorMode: { def: "default" },
cornerRadius: { def: 12, onChange: "updateNiriLayout" }, cornerRadius: { def: 12, onChange: "updateCompositorLayout" },
niriLayoutGapsOverride: { def: -1, onChange: "updateNiriLayout" }, niriLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
niriLayoutRadiusOverride: { def: -1, onChange: "updateNiriLayout" }, niriLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
niriLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
hyprlandLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
hyprlandLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
hyprlandLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
mangoLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
use24HourClock: { def: true }, use24HourClock: { def: true },
showSeconds: { def: false }, showSeconds: { def: false },
@@ -63,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 },
@@ -87,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 },
@@ -102,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: "" },
@@ -127,6 +148,10 @@ var SPEC = {
qt6ctAvailable: { def: false, persist: false }, qt6ctAvailable: { def: false, persist: false },
gtkAvailable: { def: false, persist: false }, gtkAvailable: { def: false, persist: false },
cursorSettings: { def: { theme: "System Default", size: 24, niri: { hideWhenTyping: false, hideAfterInactiveMs: 0 }, hyprland: { hideOnKeyPress: false, hideOnTouch: false, inactiveTimeout: 0 }, dwl: { cursorHideTimeout: 0 } }, onChange: "updateCompositorCursor" },
availableCursorThemes: { def: ["System Default"], persist: false },
systemDefaultCursorTheme: { def: "", persist: false },
launcherLogoMode: { def: "apps" }, launcherLogoMode: { def: "apps" },
launcherLogoCustomPath: { def: "" }, launcherLogoCustomPath: { def: "" },
launcherLogoColorOverride: { def: "" }, launcherLogoColorOverride: { def: "" },
@@ -166,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: {} },
@@ -185,6 +210,8 @@ var SPEC = {
runDmsMatugenTemplates: { def: true }, runDmsMatugenTemplates: { def: true },
matugenTemplateGtk: { def: true }, matugenTemplateGtk: { def: true },
matugenTemplateNiri: { def: true }, matugenTemplateNiri: { def: true },
matugenTemplateHyprland: { def: true },
matugenTemplateMangowc: { def: true },
matugenTemplateQt5ct: { def: true }, matugenTemplateQt5ct: { def: true },
matugenTemplateQt6ct: { def: true }, matugenTemplateQt6ct: { def: true },
matugenTemplateFirefox: { def: true }, matugenTemplateFirefox: { def: true },
@@ -236,11 +263,13 @@ var SPEC = {
fprintdAvailable: { def: false, persist: false }, fprintdAvailable: { def: false, persist: false },
lockScreenActiveMonitor: { def: "all" }, lockScreenActiveMonitor: { def: "all" },
lockScreenInactiveColor: { def: "#000000" }, lockScreenInactiveColor: { def: "#000000" },
lockScreenNotificationMode: { def: 0 },
hideBrightnessSlider: { def: false }, hideBrightnessSlider: { def: false },
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 },
@@ -322,7 +351,11 @@ var SPEC = {
maximizeDetection: true, maximizeDetection: true,
scrollEnabled: true, scrollEnabled: true,
scrollXBehavior: "column", scrollXBehavior: "column",
scrollYBehavior: "workspace" scrollYBehavior: "workspace",
shadowIntensity: 0,
shadowOpacity: 60,
shadowColorMode: "text",
shadowCustomColor: "#000000"
}], onChange: "updateBarConfigs" }, }], onChange: "updateBarConfigs" },
desktopClockEnabled: { def: false }, desktopClockEnabled: { def: false },
@@ -368,7 +401,11 @@ var SPEC = {
desktopWidgetPositions: { def: {} }, desktopWidgetPositions: { def: {} },
desktopWidgetGridSettings: { def: {} }, desktopWidgetGridSettings: { def: {} },
desktopWidgetInstances: { def: [] } desktopWidgetInstances: { def: [] },
desktopWidgetGroups: { def: [] },
builtInPluginSettings: { def: {} }
}; };
function getValidKeys() { function getValidKeys() {

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;
@@ -606,6 +636,8 @@ Item {
active: false active: false
Component.onCompleted: PopoutService.processListModalLoader = processListModalLoader
ProcessListModal { ProcessListModal {
id: processListModal id: processListModal
@@ -658,6 +690,9 @@ Item {
} }
} }
} }
onInstancesChanged: PopoutService.notepadSlideouts = instances
Component.onCompleted: PopoutService.notepadSlideouts = instances
} }
LazyLoader { LazyLoader {
@@ -831,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();
@@ -787,7 +828,16 @@ Item {
const widgets = BarWidgetService.getRegisteredWidgetIds(); const widgets = BarWidgetService.getRegisteredWidgetIds();
if (widgets.length === 0) if (widgets.length === 0)
return "No widgets registered"; return "No widgets registered";
return widgets.join("\n");
const lines = [];
for (const widgetId of widgets) {
const widget = BarWidgetService.getWidgetOnFocusedScreen(widgetId);
let state = "";
if (widget?.effectiveVisible !== undefined)
state = widget.effectiveVisible ? " [visible]" : " [hidden]";
lines.push(widgetId + state);
}
return lines.join("\n");
} }
function status(widgetId: string): string { function status(widgetId: string): string {
@@ -806,6 +856,76 @@ Item {
return "hidden"; return "hidden";
} }
function reveal(widgetId: string): string {
if (!widgetId)
return "ERROR: No widget ID specified";
if (!BarWidgetService.hasWidget(widgetId))
return `WIDGET_NOT_FOUND: ${widgetId}`;
const widget = BarWidgetService.getWidgetOnFocusedScreen(widgetId);
if (!widget)
return `WIDGET_NOT_AVAILABLE: ${widgetId}`;
if (typeof widget.setVisibilityOverride === "function") {
widget.setVisibilityOverride(true);
return `WIDGET_REVEAL_SUCCESS: ${widgetId}`;
}
return `WIDGET_REVEAL_NOT_SUPPORTED: ${widgetId}`;
}
function hide(widgetId: string): string {
if (!widgetId)
return "ERROR: No widget ID specified";
if (!BarWidgetService.hasWidget(widgetId))
return `WIDGET_NOT_FOUND: ${widgetId}`;
const widget = BarWidgetService.getWidgetOnFocusedScreen(widgetId);
if (!widget)
return `WIDGET_NOT_AVAILABLE: ${widgetId}`;
if (typeof widget.setVisibilityOverride === "function") {
widget.setVisibilityOverride(false);
return `WIDGET_HIDE_SUCCESS: ${widgetId}`;
}
return `WIDGET_HIDE_NOT_SUPPORTED: ${widgetId}`;
}
function reset(widgetId: string): string {
if (!widgetId)
return "ERROR: No widget ID specified";
if (!BarWidgetService.hasWidget(widgetId))
return `WIDGET_NOT_FOUND: ${widgetId}`;
const widget = BarWidgetService.getWidgetOnFocusedScreen(widgetId);
if (!widget)
return `WIDGET_NOT_AVAILABLE: ${widgetId}`;
if (typeof widget.clearVisibilityOverride === "function") {
widget.clearVisibilityOverride();
return `WIDGET_RESET_SUCCESS: ${widgetId}`;
}
return `WIDGET_RESET_NOT_SUPPORTED: ${widgetId}`;
}
function visibility(widgetId: string): string {
if (!widgetId)
return "ERROR: No widget ID specified";
if (!BarWidgetService.hasWidget(widgetId))
return `WIDGET_NOT_FOUND: ${widgetId}`;
const widget = BarWidgetService.getWidgetOnFocusedScreen(widgetId);
if (!widget)
return `WIDGET_NOT_AVAILABLE: ${widgetId}`;
if (widget.effectiveVisible !== undefined)
return widget.effectiveVisible ? "visible" : "hidden";
return "unknown";
}
target: "widget" target: "widget"
} }
@@ -949,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 {
@@ -960,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":
@@ -727,7 +736,7 @@ FocusScope {
id: gridScrollbar id: gridScrollbar
} }
ScrollBar.horizontal: ScrollBar { ScrollBar.horizontal: DankScrollbar {
policy: ScrollBar.AlwaysOff policy: ScrollBar.AlwaysOff
} }

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

@@ -213,7 +213,7 @@ FloatingWindow {
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankActionButton { DankActionButton {
visible: windowControls.supported visible: windowControls.supported && windowControls.canMaximize
iconName: root.maximized ? "fullscreen_exit" : "fullscreen" iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
iconSize: Theme.iconSize - 4 iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText iconColor: Theme.surfaceText

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,26 +111,25 @@ FloatingWindow {
event.accepted = true; event.accepted = true;
} }
MouseArea { Item {
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
height: headerRow.height + Theme.spacingM anchors.margins: Theme.spacingM
onPressed: windowControls.tryStartMove() height: Math.max(titleColumn.implicitHeight, windowButtonRow.implicitHeight)
onDoubleClicked: windowControls.tryToggleMaximize()
}
Row { MouseArea {
id: headerRow anchors.fill: parent
anchors.left: parent.left onPressed: windowControls.tryStartMove()
anchors.right: parent.right onDoubleClicked: windowControls.tryToggleMaximize()
anchors.top: parent.top }
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingM
Column { Column {
width: parent.width - 60 id: titleColumn
anchors.left: parent.left
anchors.right: windowButtonRow.left
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingXS spacing: Theme.spacingXS
StyledText { StyledText {
@@ -137,35 +139,38 @@ 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: windowButtonRow
anchors.right: parent.right
anchors.top: parent.top
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankActionButton { DankActionButton {
visible: windowControls.supported visible: windowControls.supported && windowControls.canMaximize
iconName: root.maximized ? "fullscreen_exit" : "fullscreen" iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
iconSize: Theme.iconSize - 4 iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
@@ -184,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 {
@@ -223,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
@@ -232,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

@@ -151,7 +151,7 @@ Item {
fileSearchController.openSelected(); fileSearchController.openSelected();
} }
event.accepted = true; event.accepted = true;
} else if (event.key === Qt.Key_Menu) { } else if (event.key === Qt.Key_Menu || event.key == Qt.Key_F10) {
if (searchMode === "apps" && appLauncher.model.count > 0) { if (searchMode === "apps" && appLauncher.model.count > 0) {
const selectedApp = appLauncher.model.get(appLauncher.selectedIndex); const selectedApp = appLauncher.model.get(appLauncher.selectedIndex);
const menu = usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item; const menu = usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item;

View File

@@ -11,12 +11,88 @@ Item {
property int selectedMenuIndex: 0 property int selectedMenuIndex: 0
property bool keyboardNavigation: false property bool keyboardNavigation: false
signal hideRequested() signal hideRequested
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
readonly property var actualItem: (currentApp && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
function getPluginContextMenuActions() {
if (!currentApp || !currentApp.isPlugin || !actualItem)
return [];
const pluginId = appLauncher.getPluginIdForItem(actualItem);
if (!pluginId) {
console.log("[ContextMenu] No pluginId found for item:", JSON.stringify(actualItem.categories));
return [];
}
const instance = PluginService.pluginInstances[pluginId];
if (!instance) {
console.log("[ContextMenu] No instance for pluginId:", pluginId);
return [];
}
if (typeof instance.getContextMenuActions !== "function") {
console.log("[ContextMenu] Instance has no getContextMenuActions:", pluginId);
return [];
}
const actions = instance.getContextMenuActions(actualItem);
if (!Array.isArray(actions))
return [];
return actions;
}
function executePluginAction(actionData) {
if (!currentApp || !actualItem)
return;
const pluginId = appLauncher.getPluginIdForItem(actualItem);
if (!pluginId)
return;
const instance = PluginService.pluginInstances[pluginId];
if (!instance)
return;
if (typeof actionData === "function") {
actionData();
} else if (typeof instance.executeContextMenuAction === "function") {
instance.executeContextMenuAction(actualItem, actionData);
}
if (appLauncher)
appLauncher.updateFilteredModel();
hideRequested();
}
readonly property var menuItems: { readonly property var menuItems: {
const items = []; const items = [];
if (currentApp && currentApp.isPlugin) {
const pluginActions = getPluginContextMenuActions();
for (let i = 0; i < pluginActions.length; i++) {
const act = pluginActions[i];
items.push({
type: "item",
icon: act.icon || "",
text: act.text || act.name || "",
action: () => executePluginAction(act.action)
});
}
if (items.length === 0) {
items.push({
type: "item",
icon: "content_copy",
text: I18n.tr("Copy"),
action: launchCurrentApp
});
}
return items;
}
const appId = desktopEntry ? (desktopEntry.id || desktopEntry.execString || "") : ""; const appId = desktopEntry ? (desktopEntry.id || desktopEntry.execString || "") : "";
const isPinned = SessionData.isPinnedApp(appId); const isPinned = SessionData.isPinnedApp(appId);
@@ -172,18 +248,25 @@ Item {
focus: keyboardNavigation focus: keyboardNavigation
Keys.onPressed: event => { Keys.onPressed: event => {
if (event.key === Qt.Key_Down) { switch (event.key) {
case Qt.Key_Down:
selectNext(); selectNext();
event.accepted = true; event.accepted = true;
} else if (event.key === Qt.Key_Up) { break;
case Qt.Key_Up:
selectPrevious(); selectPrevious();
event.accepted = true; event.accepted = true;
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { break;
case Qt.Key_Return:
case Qt.Key_Enter:
activateSelected(); activateSelected();
event.accepted = true; event.accepted = true;
} else if (event.key === Qt.Key_Escape) { break;
case Qt.Key_Escape:
case Qt.Key_Left:
hideRequested(); hideRequested();
event.accepted = true; event.accepted = true;
break;
} }
} }

View File

@@ -22,11 +22,8 @@ DankModal {
function resetContent() { function resetContent() {
if (!spotlightContent) if (!spotlightContent)
return; return;
if (spotlightContent.appLauncher) { if (spotlightContent.appLauncher)
spotlightContent.appLauncher.searchQuery = ""; spotlightContent.appLauncher.reset();
spotlightContent.appLauncher.selectedIndex = 0;
spotlightContent.appLauncher.setCategory(I18n.tr("All"));
}
if (spotlightContent.fileSearchController) if (spotlightContent.fileSearchController)
spotlightContent.fileSearchController.reset(); spotlightContent.fileSearchController.reset();
if (spotlightContent.resetScroll) if (spotlightContent.resetScroll)
@@ -41,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();
}); });
@@ -56,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,12 +44,18 @@ 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 (showUsernameField) h += inputFieldWithSpacing; if (isHiddenNetwork)
if (showPasswordField) h += inputFieldWithSpacing; h += inputFieldWithSpacing;
if (showAnonField) h += inputFieldWithSpacing; if (showUsernameField)
if (showDomainField) h += inputFieldWithSpacing; h += inputFieldWithSpacing;
if (showShowPasswordCheckbox) h += checkboxRowHeight; if (showPasswordField)
if (showSavePasswordCheckbox) h += checkboxRowHeight; h += inputFieldWithSpacing;
if (showAnonField)
h += inputFieldWithSpacing;
if (showDomainField)
h += inputFieldWithSpacing;
if (showSavePasswordCheckbox)
h += checkboxRowHeight;
return h; return h;
} }
@@ -62,6 +68,10 @@ FloatingWindow {
} }
return; return;
} }
if (isHiddenNetwork) {
ssidInput.forceActiveFocus();
return;
}
if (requiresEnterprise && !isVpnPrompt) { if (requiresEnterprise && !isVpnPrompt) {
usernameInput.forceActiveFocus(); usernameInput.forceActiveFocus();
return; return;
@@ -76,6 +86,7 @@ FloatingWindow {
wifiAnonymousIdentityInput = ""; wifiAnonymousIdentityInput = "";
wifiDomainInput = ""; wifiDomainInput = "";
isPromptMode = false; isPromptMode = false;
isHiddenNetwork = false;
promptToken = ""; promptToken = "";
promptReason = ""; promptReason = "";
promptFields = []; promptFields = [];
@@ -94,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;
@@ -178,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();
@@ -190,6 +226,8 @@ FloatingWindow {
passwordInput.text = ""; passwordInput.text = "";
if (requiresEnterprise) if (requiresEnterprise)
usernameInput.text = ""; usernameInput.text = "";
if (isHiddenNetwork)
ssidInput.text = "";
} }
function clearAndClose() { function clearAndClose() {
@@ -209,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)
@@ -230,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])
@@ -267,11 +308,14 @@ FloatingWindow {
width: parent.width - Theme.spacingL * 2 width: parent.width - Theme.spacingL * 2
spacing: Theme.spacingM spacing: Theme.spacingM
Row { Item {
width: contentCol.width width: contentCol.width
height: Math.max(headerCol.height, buttonRow.height)
MouseArea { MouseArea {
width: parent.width - 60 anchors.left: parent.left
anchors.right: buttonRow.left
anchors.rightMargin: Theme.spacingM
height: headerCol.height height: headerCol.height
onPressed: windowControls.tryStartMove() onPressed: windowControls.tryStartMove()
onDoubleClicked: windowControls.tryToggleMaximize() onDoubleClicked: windowControls.tryToggleMaximize()
@@ -287,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
@@ -306,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;
} }
@@ -327,10 +375,12 @@ FloatingWindow {
} }
Row { Row {
id: buttonRow
anchors.right: parent.right
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankActionButton { DankActionButton {
visible: windowControls.supported visible: windowControls.supported && windowControls.canMaximize
iconName: root.maximized ? "fullscreen_exit" : "fullscreen" iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
iconSize: Theme.iconSize - 4 iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
@@ -346,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
@@ -366,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
@@ -468,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
@@ -547,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
}
} }
} }
@@ -685,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

@@ -96,7 +96,11 @@ Item {
_updatingFromTrigger = true; _updatingFromTrigger = true;
selectedCategory = triggerResult.pluginCategory; selectedCategory = triggerResult.pluginCategory;
_updatingFromTrigger = false; _updatingFromTrigger = false;
apps = AppSearchService.getPluginItems(triggerResult.pluginCategory, triggerResult.query); if (triggerResult.isBuiltIn) {
apps = AppSearchService.getBuiltInLauncherItems(triggerResult.pluginId, triggerResult.query);
} else {
apps = AppSearchService.getPluginItems(triggerResult.pluginCategory, triggerResult.query);
}
} else { } else {
if (_isTriggered) { if (_isTriggered) {
_updatingFromTrigger = true; _updatingFromTrigger = true;
@@ -114,7 +118,11 @@ Item {
const items = AppSearchService.getPluginItems(pluginCategory, ""); const items = AppSearchService.getPluginItems(pluginCategory, "");
emptyTriggerItems = emptyTriggerItems.concat(items); emptyTriggerItems = emptyTriggerItems.concat(items);
}); });
// Add Core Apps const builtInEmptyTrigger = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger();
builtInEmptyTrigger.forEach(pluginId => {
const items = AppSearchService.getBuiltInLauncherItems(pluginId, "");
emptyTriggerItems = emptyTriggerItems.concat(items);
});
const coreItems = AppSearchService.getCoreApps(""); const coreItems = AppSearchService.getCoreApps("");
apps = AppSearchService.applications.concat(emptyTriggerItems).concat(coreItems); apps = AppSearchService.applications.concat(emptyTriggerItems).concat(coreItems);
} else { } else {
@@ -133,6 +141,11 @@ Item {
const items = AppSearchService.getPluginItems(pluginCategory, searchQuery); const items = AppSearchService.getPluginItems(pluginCategory, searchQuery);
emptyTriggerItems = emptyTriggerItems.concat(items); emptyTriggerItems = emptyTriggerItems.concat(items);
}); });
const builtInEmptyTrigger = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger();
builtInEmptyTrigger.forEach(pluginId => {
const items = AppSearchService.getBuiltInLauncherItems(pluginId, searchQuery);
emptyTriggerItems = emptyTriggerItems.concat(items);
});
const coreItems = AppSearchService.getCoreApps(searchQuery); const coreItems = AppSearchService.getCoreApps(searchQuery);
apps = apps.concat(emptyTriggerItems).concat(coreItems); apps = apps.concat(emptyTriggerItems).concat(coreItems);
@@ -186,12 +199,14 @@ Item {
filteredModel.append({ filteredModel.append({
"name": app.name || "", "name": app.name || "",
"exec": app.execString || app.exec || app.action || "", "exec": app.execString || app.exec || app.action || "",
"icon": app.icon !== undefined ? app.icon : (isPluginItem ? "" : "application-x-executable"), "icon": app.icon !== undefined ? String(app.icon) : (isPluginItem ? "" : "application-x-executable"),
"comment": app.comment || "", "comment": app.comment || "",
"categories": app.categories || [], "categories": app.categories || [],
"isPlugin": isPluginItem, "isPlugin": isPluginItem,
"isCore": app.isCore === true, "isCore": app.isCore === true,
"appIndex": uniqueApps.length - 1 "isBuiltInLauncher": app.isBuiltInLauncher === true,
"appIndex": uniqueApps.length - 1,
"pinned": app._pinned === true
}); });
} }
}); });
@@ -240,13 +255,18 @@ Item {
} }
function launchApp(appData) { function launchApp(appData) {
if (!appData || typeof appData.appIndex === "undefined" || appData.appIndex < 0 || appData.appIndex >= _uniqueApps.length) { if (!appData || typeof appData.appIndex === "undefined" || appData.appIndex < 0 || appData.appIndex >= _uniqueApps.length)
return; return;
}
suppressUpdatesWhileLaunching = true; suppressUpdatesWhileLaunching = true;
const actualApp = _uniqueApps[appData.appIndex]; const actualApp = _uniqueApps[appData.appIndex];
if (appData.isBuiltInLauncher) {
AppSearchService.executeBuiltInLauncherItem(actualApp);
appLaunched(appData);
return;
}
if (appData.isCore) { if (appData.isCore) {
AppSearchService.executeCoreApp(actualApp); AppSearchService.executeCoreApp(actualApp);
appLaunched(appData); appLaunched(appData);
@@ -260,11 +280,20 @@ Item {
appLaunched(appData); appLaunched(appData);
return; return;
} }
} else { return;
SessionService.launchDesktopEntry(actualApp);
appLaunched(appData);
AppUsageHistoryData.addAppUsage(actualApp);
} }
SessionService.launchDesktopEntry(actualApp);
appLaunched(appData);
AppUsageHistoryData.addAppUsage(actualApp);
}
function reset() {
suppressUpdatesWhileLaunching = false;
searchQuery = "";
selectedIndex = 0;
setCategory(I18n.tr("All"));
updateFilteredModel();
} }
function setCategory(category) { function setCategory(category) {
@@ -320,35 +349,55 @@ Item {
onTriggered: updateFilteredModel() onTriggered: updateFilteredModel()
} }
// Plugin trigger system functions
function checkPluginTriggers(query) { function checkPluginTriggers(query) {
if (!query || typeof PluginService === "undefined") { if (!query)
return { return {
triggered: false, triggered: false,
pluginCategory: "", pluginCategory: "",
query: "" query: ""
}; };
const builtInTriggers = AppSearchService.getBuiltInLauncherTriggers();
for (const trigger in builtInTriggers) {
if (!query.startsWith(trigger))
continue;
const pluginId = builtInTriggers[trigger];
const plugin = AppSearchService.builtInPlugins[pluginId];
if (!plugin)
continue;
return {
triggered: true,
pluginId: pluginId,
pluginCategory: plugin.name,
query: query.substring(trigger.length).trim(),
trigger: trigger,
isBuiltIn: true
};
} }
if (typeof PluginService === "undefined")
return {
triggered: false,
pluginCategory: "",
query: ""
};
const triggers = PluginService.getAllPluginTriggers(); const triggers = PluginService.getAllPluginTriggers();
for (const trigger in triggers) { for (const trigger in triggers) {
if (query.startsWith(trigger)) { if (!query.startsWith(trigger))
const pluginId = triggers[trigger]; continue;
const plugin = PluginService.getLauncherPlugin(pluginId); const pluginId = triggers[trigger];
const plugin = PluginService.getLauncherPlugin(pluginId);
if (plugin) { if (!plugin)
const remainingQuery = query.substring(trigger.length).trim(); continue;
const result = { return {
triggered: true, triggered: true,
pluginId: pluginId, pluginId: pluginId,
pluginCategory: plugin.name || pluginId, pluginCategory: plugin.name || pluginId,
query: remainingQuery, query: query.substring(trigger.length).trim(),
trigger: trigger trigger: trigger,
}; isBuiltIn: false
return result; };
}
}
} }
return { return {

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

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