mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-27 06:52:50 -05:00
Compare commits
113 Commits
c1d95a3086
...
stable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7cdb39b0b | ||
|
|
0ceba92a23 | ||
|
|
4daa7a4c88 | ||
|
|
cc4a6a5899 | ||
|
|
994947477c | ||
|
|
311817ee97 | ||
|
|
b80c73f9b9 | ||
|
|
a85101c099 | ||
|
|
3513d57e06 | ||
|
|
1234847abb | ||
|
|
0ed595b43d | ||
|
|
060cbefc79 | ||
|
|
e022c04519 | ||
|
|
f534384e5e | ||
|
|
a25cdb43d5 | ||
|
|
4e9b4ca400 | ||
|
|
5bab1c98b1 | ||
|
|
2284bb002f | ||
|
|
b0611d6104 | ||
|
|
27965862d6 | ||
|
|
e74a901e05 | ||
|
|
77794deb2c | ||
|
|
1c10746e50 | ||
|
|
8ecb7282b9 | ||
|
|
9b3fa804ab | ||
|
|
b2ad31a27e | ||
|
|
db17e4cb14 | ||
|
|
1b7dcf56a8 | ||
|
|
502bb88e92 | ||
|
|
b76d0ce97d | ||
|
|
fa66d330cf | ||
|
|
157eab2d07 | ||
|
|
f50ad2dc22 | ||
|
|
cd9d92d884 | ||
|
|
1b69a5e62b | ||
|
|
61d311b157 | ||
|
|
6b76b86930 | ||
|
|
dcfb947c36 | ||
|
|
59893b7f44 | ||
|
|
d2c62f5533 | ||
|
|
2bbe9a0c45 | ||
|
|
4e2ce82c0a | ||
|
|
104762186f | ||
|
|
f1233ab1e3 | ||
|
|
d6b407ec37 | ||
|
|
022b4b4bb3 | ||
|
|
49b322582d | ||
|
|
1280bd047d | ||
|
|
6f206d7523 | ||
|
|
2e58283859 | ||
|
|
99a5721fe8 | ||
|
|
5302ebd840 | ||
|
|
fa427ea1ac | ||
|
|
7027bd1646 | ||
|
|
3c38e17472 | ||
|
|
510ea5d2e4 | ||
|
|
bb2234d328 | ||
|
|
edbdeb0fb8 | ||
|
|
19541fc573 | ||
|
|
7c936cacfb | ||
|
|
c60cd3a341 | ||
|
|
e37135f80d | ||
|
|
aac937cbcc | ||
|
|
4b46d022af | ||
|
|
7f0181b310 | ||
|
|
6a109274f8 | ||
|
|
0f09cc693a | ||
|
|
af0166a553 | ||
|
|
a283017f26 | ||
|
|
5ae2cd1dfb | ||
|
|
eece811fb0 | ||
|
|
1ff1f3a7f2 | ||
|
|
a21a846bf5 | ||
|
|
f5f21e738a | ||
|
|
033e62418a | ||
|
|
3c69e8b1cc | ||
|
|
118be27796 | ||
|
|
721d35d417 | ||
|
|
7bc3d5910d | ||
|
|
ccc7047be0 | ||
|
|
a5e107c89d | ||
|
|
646d60dcbf | ||
|
|
5dc7c0d797 | ||
|
|
db1de9df38 | ||
|
|
3dd21382ba | ||
|
|
ec2b3d0d4b | ||
|
|
a205df1bd6 | ||
|
|
e822fa73da | ||
|
|
634e75b80c | ||
|
|
ec5b507efc | ||
|
|
e6d289d48c | ||
|
|
745d7f26ce | ||
|
|
ad43053b94 | ||
|
|
721700190b | ||
|
|
8c9c936d0e | ||
|
|
842bf6e3ff | ||
|
|
c1fbeb3f5e | ||
|
|
c45eb2cccf | ||
|
|
1b5abca83a | ||
|
|
45818b202f | ||
|
|
1c8ce46f25 | ||
|
|
f762f9ae49 | ||
|
|
4484f6bd61 | ||
|
|
0076c45496 | ||
|
|
ab071e12aa | ||
|
|
8386b40c50 | ||
|
|
03a985228d | ||
|
|
ef7d7ec13d | ||
|
|
824792cca7 | ||
|
|
850e5b6572 | ||
|
|
64310854a6 | ||
|
|
4005a55bf2 | ||
|
|
0236fe3276 |
20
.github/workflows/stable.yml
vendored
20
.github/workflows/stable.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -108,3 +108,5 @@ bin/
|
|||||||
# direnv
|
# direnv
|
||||||
.envrc
|
.envrc
|
||||||
.direnv/
|
.direnv/
|
||||||
|
quickshell/dms-plugins
|
||||||
|
__pycache__
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -514,5 +514,6 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
matugenCmd,
|
matugenCmd,
|
||||||
clipboardCmd,
|
clipboardCmd,
|
||||||
doctorCmd,
|
doctorCmd,
|
||||||
|
configCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
318
core/cmd/dms/commands_config.go
Normal file
318
core/cmd/dms/commands_config.go
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configCmd = &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Configuration utilities",
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolveIncludeCmd = &cobra.Command{
|
||||||
|
Use: "resolve-include <compositor> <filename>",
|
||||||
|
Short: "Check if a file is included in compositor config",
|
||||||
|
Long: "Recursively check if a file is included/sourced in compositor configuration. Returns JSON with exists and included status.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
switch len(args) {
|
||||||
|
case 0:
|
||||||
|
return []string{"hyprland", "niri", "mangowc"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
case 1:
|
||||||
|
return []string{"cursor.kdl", "cursor.conf", "outputs.kdl", "outputs.conf", "binds.kdl", "binds.conf"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runResolveInclude,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
configCmd.AddCommand(resolveIncludeCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IncludeResult struct {
|
||||||
|
Exists bool `json:"exists"`
|
||||||
|
Included bool `json:"included"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func runResolveInclude(cmd *cobra.Command, args []string) {
|
||||||
|
compositor := strings.ToLower(args[0])
|
||||||
|
filename := args[1]
|
||||||
|
|
||||||
|
var result IncludeResult
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch compositor {
|
||||||
|
case "hyprland":
|
||||||
|
result, err = checkHyprlandInclude(filename)
|
||||||
|
case "niri":
|
||||||
|
result, err = checkNiriInclude(filename)
|
||||||
|
case "mangowc", "dwl", "mango":
|
||||||
|
result, err = checkMangoWCInclude(filename)
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown compositor: %s", compositor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error checking include: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := json.Marshal(result)
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkHyprlandInclude(filename string) (IncludeResult, error) {
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||||
|
if err != nil {
|
||||||
|
return IncludeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
|
result := IncludeResult{}
|
||||||
|
|
||||||
|
if _, err := os.Stat(targetPath); err == nil {
|
||||||
|
result.Exists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
result.Included = hyprlandFindInclude(mainConfig, "dms/"+filename, processed)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hyprlandFindInclude(filePath, target string, processed map[string]bool) bool {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if processed[absPath] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
processed[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDir := filepath.Dir(absPath)
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(trimmed, "source") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcePath := strings.TrimSpace(parts[1])
|
||||||
|
if matchesTarget(sourcePath, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := sourcePath
|
||||||
|
if !filepath.IsAbs(sourcePath) {
|
||||||
|
fullPath = filepath.Join(baseDir, sourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded, err := utils.ExpandPath(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if hyprlandFindInclude(expanded, target, processed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkNiriInclude(filename string) (IncludeResult, error) {
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||||
|
if err != nil {
|
||||||
|
return IncludeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
|
result := IncludeResult{}
|
||||||
|
|
||||||
|
if _, err := os.Stat(targetPath); err == nil {
|
||||||
|
result.Exists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(configDir, "config.kdl")
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
result.Included = niriFindInclude(mainConfig, "dms/"+filename, processed)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func niriFindInclude(filePath, target string, processed map[string]bool) bool {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if processed[absPath] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
processed[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDir := filepath.Dir(absPath)
|
||||||
|
content := string(data)
|
||||||
|
|
||||||
|
for _, line := range strings.Split(content, "\n") {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "//") || trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(trimmed, "include") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
startQuote := strings.Index(trimmed, "\"")
|
||||||
|
if startQuote == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
endQuote := strings.LastIndex(trimmed, "\"")
|
||||||
|
if endQuote <= startQuote {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
includePath := trimmed[startQuote+1 : endQuote]
|
||||||
|
if matchesTarget(includePath, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := includePath
|
||||||
|
if !filepath.IsAbs(includePath) {
|
||||||
|
fullPath = filepath.Join(baseDir, includePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if niriFindInclude(fullPath, target, processed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMangoWCInclude(filename string) (IncludeResult, error) {
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/mango")
|
||||||
|
if err != nil {
|
||||||
|
return IncludeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
|
result := IncludeResult{}
|
||||||
|
|
||||||
|
if _, err := os.Stat(targetPath); err == nil {
|
||||||
|
result.Exists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(configDir, "config.conf")
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
mainConfig = filepath.Join(configDir, "mango.conf")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
result.Included = mangowcFindInclude(mainConfig, "dms/"+filename, processed)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mangowcFindInclude(filePath, target string, processed map[string]bool) bool {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if processed[absPath] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
processed[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDir := filepath.Dir(absPath)
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(trimmed, "source") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcePath := strings.TrimSpace(parts[1])
|
||||||
|
if matchesTarget(sourcePath, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := sourcePath
|
||||||
|
if !filepath.IsAbs(sourcePath) {
|
||||||
|
fullPath = filepath.Join(baseDir, sourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded, err := utils.ExpandPath(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if mangowcFindInclude(expanded, target, processed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesTarget(path, target string) bool {
|
||||||
|
path = strings.TrimPrefix(path, "./")
|
||||||
|
target = strings.TrimPrefix(target, "./")
|
||||||
|
return path == target || strings.HasSuffix(path, "/"+target)
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
|||||||
26
core/go.mod
26
core/go.mod
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
29
core/go.sum
29
core/go.sum
@@ -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=
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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, "")
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
156
core/internal/config/embedded/hypr-binds.conf
Normal file
156
core/internal/config/embedded/hypr-binds.conf
Normal 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
|
||||||
25
core/internal/config/embedded/hypr-colors.conf
Normal file
25
core/internal/config/embedded/hypr-colors.conf
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
11
core/internal/config/embedded/hypr-layout.conf
Normal file
11
core/internal/config/embedded/hypr-layout.conf
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, " ")
|
||||||
|
|||||||
@@ -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"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package network
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -925,25 +926,24 @@ func (b *NetworkManagerBackend) ImportVPN(filePath string, name string) (*VPNImp
|
|||||||
func (b *NetworkManagerBackend) importVPNWithNmcli(filePath string, name string) (*VPNImportResult, error) {
|
func (b *NetworkManagerBackend) importVPNWithNmcli(filePath string, name string) (*VPNImportResult, error) {
|
||||||
vpnTypes := []string{"openvpn", "wireguard", "vpnc", "pptp", "l2tp", "openconnect", "strongswan"}
|
vpnTypes := []string{"openvpn", "wireguard", "vpnc", "pptp", "l2tp", "openconnect", "strongswan"}
|
||||||
|
|
||||||
var output []byte
|
var allErrors []error
|
||||||
var err error
|
var outputStr string
|
||||||
for _, vpnType := range vpnTypes {
|
for _, vpnType := range vpnTypes {
|
||||||
args := []string{"connection", "import", "type", vpnType, "file", filePath}
|
cmd := exec.Command("nmcli", "connection", "import", "type", vpnType, "file", filePath)
|
||||||
cmd := exec.Command("nmcli", args...)
|
output, err := cmd.CombinedOutput()
|
||||||
output, err = cmd.CombinedOutput()
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
outputStr = string(output)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
allErrors = append(allErrors, fmt.Errorf("%s: %s", vpnType, strings.TrimSpace(string(output))))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if len(allErrors) == len(vpnTypes) {
|
||||||
return &VPNImportResult{
|
return &VPNImportResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Error: fmt.Sprintf("import failed: %s", strings.TrimSpace(string(output))),
|
Error: errors.Join(allErrors...).Error(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
outputStr := string(output)
|
|
||||||
var connUUID, connName string
|
var connUUID, connName string
|
||||||
|
|
||||||
lines := strings.Split(outputStr, "\n")
|
lines := strings.Split(outputStr, "\n")
|
||||||
|
|||||||
@@ -357,31 +357,51 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
|||||||
|
|
||||||
savedSSIDs := make(map[string]bool)
|
savedSSIDs := make(map[string]bool)
|
||||||
autoconnectMap := make(map[string]bool)
|
autoconnectMap := make(map[string]bool)
|
||||||
|
hiddenSSIDs := make(map[string]bool)
|
||||||
for _, conn := range connections {
|
for _, conn := range connections {
|
||||||
connSettings, err := conn.GetSettings()
|
connSettings, err := conn.GetSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if connMeta, ok := connSettings["connection"]; ok {
|
connMeta, ok := connSettings["connection"]
|
||||||
if connType, ok := connMeta["type"].(string); ok && connType == "802-11-wireless" {
|
if !ok {
|
||||||
if wifiSettings, ok := connSettings["802-11-wireless"]; ok {
|
continue
|
||||||
if ssidBytes, ok := wifiSettings["ssid"].([]byte); ok {
|
}
|
||||||
ssid := string(ssidBytes)
|
|
||||||
savedSSIDs[ssid] = true
|
connType, ok := connMeta["type"].(string)
|
||||||
autoconnect := true
|
if !ok || connType != "802-11-wireless" {
|
||||||
if ac, ok := connMeta["autoconnect"].(bool); ok {
|
continue
|
||||||
autoconnect = ac
|
}
|
||||||
}
|
|
||||||
autoconnectMap[ssid] = autoconnect
|
wifiSettings, ok := connSettings["802-11-wireless"]
|
||||||
}
|
if !ok {
|
||||||
}
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ssidBytes, ok := wifiSettings["ssid"].([]byte)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ssid := string(ssidBytes)
|
||||||
|
savedSSIDs[ssid] = true
|
||||||
|
autoconnect := true
|
||||||
|
if ac, ok := connMeta["autoconnect"].(bool); ok {
|
||||||
|
autoconnect = ac
|
||||||
|
}
|
||||||
|
autoconnectMap[ssid] = autoconnect
|
||||||
|
|
||||||
|
if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden {
|
||||||
|
hiddenSSIDs[ssid] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b.stateMutex.RLock()
|
b.stateMutex.RLock()
|
||||||
currentSSID := b.state.WiFiSSID
|
currentSSID := b.state.WiFiSSID
|
||||||
|
wifiConnected := b.state.WiFiConnected
|
||||||
|
wifiSignal := b.state.WiFiSignal
|
||||||
|
wifiBSSID := b.state.WiFiBSSID
|
||||||
b.stateMutex.RUnlock()
|
b.stateMutex.RUnlock()
|
||||||
|
|
||||||
seenSSIDs := make(map[string]*WiFiNetwork)
|
seenSSIDs := make(map[string]*WiFiNetwork)
|
||||||
@@ -444,6 +464,7 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
|||||||
Connected: ssid == currentSSID,
|
Connected: ssid == currentSSID,
|
||||||
Saved: savedSSIDs[ssid],
|
Saved: savedSSIDs[ssid],
|
||||||
Autoconnect: autoconnectMap[ssid],
|
Autoconnect: autoconnectMap[ssid],
|
||||||
|
Hidden: hiddenSSIDs[ssid],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: maxBitrate / 1000,
|
||||||
@@ -454,6 +475,23 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
|||||||
networks = append(networks, network)
|
networks = append(networks, network)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if wifiConnected && currentSSID != "" {
|
||||||
|
if _, exists := seenSSIDs[currentSSID]; !exists {
|
||||||
|
hiddenNetwork := WiFiNetwork{
|
||||||
|
SSID: currentSSID,
|
||||||
|
BSSID: wifiBSSID,
|
||||||
|
Signal: wifiSignal,
|
||||||
|
Secured: true,
|
||||||
|
Connected: true,
|
||||||
|
Saved: savedSSIDs[currentSSID],
|
||||||
|
Autoconnect: autoconnectMap[currentSSID],
|
||||||
|
Hidden: true,
|
||||||
|
Mode: "infrastructure",
|
||||||
|
}
|
||||||
|
networks = append(networks, hiddenNetwork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sortWiFiNetworks(networks)
|
sortWiFiNetworks(networks)
|
||||||
|
|
||||||
b.stateMutex.Lock()
|
b.stateMutex.Lock()
|
||||||
@@ -515,40 +553,53 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
|
|||||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||||
dev := devInfo.device
|
dev := devInfo.device
|
||||||
w := devInfo.wireless
|
w := devInfo.wireless
|
||||||
apPaths, err := w.GetAccessPoints()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get access points: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetAP gonetworkmanager.AccessPoint
|
var targetAP gonetworkmanager.AccessPoint
|
||||||
for _, ap := range apPaths {
|
var flags, wpaFlags, rsnFlags uint32
|
||||||
ssid, err := ap.GetPropertySSID()
|
|
||||||
if err != nil || ssid != req.SSID {
|
if !req.Hidden {
|
||||||
continue
|
apPaths, err := w.GetAccessPoints()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get access points: %w", err)
|
||||||
}
|
}
|
||||||
targetAP = ap
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if targetAP == nil {
|
for _, ap := range apPaths {
|
||||||
return fmt.Errorf("access point not found: %s", req.SSID)
|
ssid, err := ap.GetPropertySSID()
|
||||||
}
|
if err != nil || ssid != req.SSID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
targetAP = ap
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
flags, _ := targetAP.GetPropertyFlags()
|
if targetAP == nil {
|
||||||
wpaFlags, _ := targetAP.GetPropertyWPAFlags()
|
return fmt.Errorf("access point not found: %s", req.SSID)
|
||||||
rsnFlags, _ := targetAP.GetPropertyRSNFlags()
|
}
|
||||||
|
|
||||||
|
flags, _ = targetAP.GetPropertyFlags()
|
||||||
|
wpaFlags, _ = targetAP.GetPropertyWPAFlags()
|
||||||
|
rsnFlags, _ = targetAP.GetPropertyRSNFlags()
|
||||||
|
}
|
||||||
|
|
||||||
const KeyMgmt8021x = uint32(512)
|
const KeyMgmt8021x = uint32(512)
|
||||||
const KeyMgmtPsk = uint32(256)
|
const KeyMgmtPsk = uint32(256)
|
||||||
const KeyMgmtSae = uint32(1024)
|
const KeyMgmtSae = uint32(1024)
|
||||||
|
|
||||||
isEnterprise := (wpaFlags&KeyMgmt8021x) != 0 || (rsnFlags&KeyMgmt8021x) != 0
|
var isEnterprise, isPsk, isSae, secured bool
|
||||||
isPsk := (wpaFlags&KeyMgmtPsk) != 0 || (rsnFlags&KeyMgmtPsk) != 0
|
|
||||||
isSae := (wpaFlags&KeyMgmtSae) != 0 || (rsnFlags&KeyMgmtSae) != 0
|
|
||||||
|
|
||||||
secured := flags != uint32(gonetworkmanager.Nm80211APFlagsNone) ||
|
switch {
|
||||||
wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) ||
|
case req.Hidden:
|
||||||
rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone)
|
secured = req.Password != "" || req.Username != ""
|
||||||
|
isEnterprise = req.Username != ""
|
||||||
|
isPsk = req.Password != "" && !isEnterprise
|
||||||
|
default:
|
||||||
|
isEnterprise = (wpaFlags&KeyMgmt8021x) != 0 || (rsnFlags&KeyMgmt8021x) != 0
|
||||||
|
isPsk = (wpaFlags&KeyMgmtPsk) != 0 || (rsnFlags&KeyMgmtPsk) != 0
|
||||||
|
isSae = (wpaFlags&KeyMgmtSae) != 0 || (rsnFlags&KeyMgmtSae) != 0
|
||||||
|
secured = flags != uint32(gonetworkmanager.Nm80211APFlagsNone) ||
|
||||||
|
wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) ||
|
||||||
|
rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone)
|
||||||
|
}
|
||||||
|
|
||||||
if isEnterprise {
|
if isEnterprise {
|
||||||
log.Infof("[createAndConnectWiFi] Enterprise network detected (802.1x) - SSID: %s, interactive: %v",
|
log.Infof("[createAndConnectWiFi] Enterprise network detected (802.1x) - SSID: %s, interactive: %v",
|
||||||
@@ -567,11 +618,15 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
|
|||||||
settings["ipv6"] = map[string]any{"method": "auto"}
|
settings["ipv6"] = map[string]any{"method": "auto"}
|
||||||
|
|
||||||
if secured {
|
if secured {
|
||||||
settings["802-11-wireless"] = map[string]any{
|
wifiSettings := map[string]any{
|
||||||
"ssid": []byte(req.SSID),
|
"ssid": []byte(req.SSID),
|
||||||
"mode": "infrastructure",
|
"mode": "infrastructure",
|
||||||
"security": "802-11-wireless-security",
|
"security": "802-11-wireless-security",
|
||||||
}
|
}
|
||||||
|
if req.Hidden {
|
||||||
|
wifiSettings["hidden"] = true
|
||||||
|
}
|
||||||
|
settings["802-11-wireless"] = wifiSettings
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case isEnterprise || req.Username != "":
|
case isEnterprise || req.Username != "":
|
||||||
@@ -658,10 +713,14 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
|
|||||||
return fmt.Errorf("secured network but not SAE/PSK/802.1X (rsn=0x%x wpa=0x%x)", rsnFlags, wpaFlags)
|
return fmt.Errorf("secured network but not SAE/PSK/802.1X (rsn=0x%x wpa=0x%x)", rsnFlags, wpaFlags)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
settings["802-11-wireless"] = map[string]any{
|
wifiSettings := map[string]any{
|
||||||
"ssid": []byte(req.SSID),
|
"ssid": []byte(req.SSID),
|
||||||
"mode": "infrastructure",
|
"mode": "infrastructure",
|
||||||
}
|
}
|
||||||
|
if req.Hidden {
|
||||||
|
wifiSettings["hidden"] = true
|
||||||
|
}
|
||||||
|
settings["802-11-wireless"] = wifiSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Interactive {
|
if req.Interactive {
|
||||||
@@ -685,14 +744,23 @@ func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionReque
|
|||||||
log.Infof("[createAndConnectWiFi] Enterprise connection added, activating (secret agent will be called)")
|
log.Infof("[createAndConnectWiFi] Enterprise connection added, activating (secret agent will be called)")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = nm.ActivateWirelessConnection(conn, dev, targetAP)
|
if req.Hidden {
|
||||||
|
_, err = nm.ActivateConnection(conn, dev, nil)
|
||||||
|
} else {
|
||||||
|
_, err = nm.ActivateWirelessConnection(conn, dev, targetAP)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to activate connection: %w", err)
|
return fmt.Errorf("failed to activate connection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("[createAndConnectWiFi] Connection activation initiated, waiting for NetworkManager state changes...")
|
log.Infof("[createAndConnectWiFi] Connection activation initiated, waiting for NetworkManager state changes...")
|
||||||
} else {
|
} else {
|
||||||
_, err = nm.AddAndActivateWirelessConnection(settings, dev, targetAP)
|
var err error
|
||||||
|
if req.Hidden {
|
||||||
|
_, err = nm.AddAndActivateConnection(settings, dev)
|
||||||
|
} else {
|
||||||
|
_, err = nm.AddAndActivateWirelessConnection(settings, dev, targetAP)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to connect: %w", err)
|
return fmt.Errorf("failed to connect: %w", err)
|
||||||
}
|
}
|
||||||
@@ -813,6 +881,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
|
|
||||||
savedSSIDs := make(map[string]bool)
|
savedSSIDs := make(map[string]bool)
|
||||||
autoconnectMap := make(map[string]bool)
|
autoconnectMap := make(map[string]bool)
|
||||||
|
hiddenSSIDs := make(map[string]bool)
|
||||||
for _, conn := range connections {
|
for _, conn := range connections {
|
||||||
connSettings, err := conn.GetSettings()
|
connSettings, err := conn.GetSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -846,6 +915,10 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
autoconnect = ac
|
autoconnect = ac
|
||||||
}
|
}
|
||||||
autoconnectMap[ssid] = autoconnect
|
autoconnectMap[ssid] = autoconnect
|
||||||
|
|
||||||
|
if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden {
|
||||||
|
hiddenSSIDs[ssid] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var devices []WiFiDevice
|
var devices []WiFiDevice
|
||||||
@@ -939,6 +1012,7 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
Connected: connected && apSSID == ssid,
|
Connected: connected && apSSID == ssid,
|
||||||
Saved: savedSSIDs[apSSID],
|
Saved: savedSSIDs[apSSID],
|
||||||
Autoconnect: autoconnectMap[apSSID],
|
Autoconnect: autoconnectMap[apSSID],
|
||||||
|
Hidden: hiddenSSIDs[apSSID],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: maxBitrate / 1000,
|
||||||
@@ -949,6 +1023,25 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
seenSSIDs[apSSID] = &network
|
seenSSIDs[apSSID] = &network
|
||||||
networks = append(networks, network)
|
networks = append(networks, network)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if connected && ssid != "" {
|
||||||
|
if _, exists := seenSSIDs[ssid]; !exists {
|
||||||
|
hiddenNetwork := WiFiNetwork{
|
||||||
|
SSID: ssid,
|
||||||
|
BSSID: bssid,
|
||||||
|
Signal: signal,
|
||||||
|
Secured: true,
|
||||||
|
Connected: true,
|
||||||
|
Saved: savedSSIDs[ssid],
|
||||||
|
Autoconnect: autoconnectMap[ssid],
|
||||||
|
Hidden: true,
|
||||||
|
Mode: "infrastructure",
|
||||||
|
Device: name,
|
||||||
|
}
|
||||||
|
networks = append(networks, hiddenNetwork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sortWiFiNetworks(networks)
|
sortWiFiNetworks(networks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
20
core/internal/utils/dbus.go
Normal file
20
core/internal/utils/dbus.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 != { }) {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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).";
|
||||||
|
|||||||
16
flake.nix
16
flake.nix
@@ -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; [
|
||||||
|
|||||||
@@ -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 => {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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(" ");
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) : "";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 "";
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
246
quickshell/Modals/Changelog/ChangelogContent.qml
Normal file
246
quickshell/Modals/Changelog/ChangelogContent.qml
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property real logoSize: Math.round(Theme.iconSize * 2.8)
|
||||||
|
readonly property real badgeHeight: Math.round(Theme.fontSizeSmall * 1.7)
|
||||||
|
|
||||||
|
topPadding: Theme.spacingL
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Image {
|
||||||
|
width: root.logoSize
|
||||||
|
height: width * (569.94629 / 506.50931)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
|
asynchronous: true
|
||||||
|
source: "file://" + Theme.shellDir + "/assets/danklogonormal.svg"
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: true
|
||||||
|
layer.mipmap: true
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
saturation: 0
|
||||||
|
colorization: 1
|
||||||
|
colorizationColor: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "DMS " + ChangelogService.currentVersion
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge + 2
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: codenameText.implicitWidth + Theme.spacingM * 2
|
||||||
|
height: root.badgeHeight
|
||||||
|
radius: root.badgeHeight / 2
|
||||||
|
color: Theme.primaryContainer
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: codenameText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Spicy Miso"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Desktop widgets, theme registry, native clipboard & more"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Theme.outlineMedium
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "What's New"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
Grid {
|
||||||
|
width: parent.width
|
||||||
|
columns: 2
|
||||||
|
rowSpacing: Theme.spacingS
|
||||||
|
columnSpacing: Theme.spacingS
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "widgets"
|
||||||
|
title: "Desktop Widgets"
|
||||||
|
description: "Widgets on your desktop"
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("desktop_widgets")
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "palette"
|
||||||
|
title: "Theme Registry"
|
||||||
|
description: "Community themes"
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("theme")
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "content_paste"
|
||||||
|
title: "Native Clipboard"
|
||||||
|
description: "Zero-dependency history"
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("clipboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "display_settings"
|
||||||
|
title: "Monitor Config"
|
||||||
|
description: "Full display setup"
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("display_config")
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "notifications_active"
|
||||||
|
title: "Notifications"
|
||||||
|
description: "History & gestures"
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("notifications")
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "healing"
|
||||||
|
title: "DMS Doctor"
|
||||||
|
description: "Diagnose issues"
|
||||||
|
onClicked: FirstLaunchService.showDoctor()
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "keyboard"
|
||||||
|
title: "Keybinds Editor"
|
||||||
|
description: "niri, Hyprland, & MangoWC"
|
||||||
|
visible: KeybindsService.available
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("keybinds")
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "search"
|
||||||
|
title: "Settings Search"
|
||||||
|
description: "Find settings fast"
|
||||||
|
onClicked: PopoutService.openSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Theme.outlineMedium
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "warning"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.warning
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "Upgrade Notes"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: upgradeNotesColumn.height + Theme.spacingM * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.warning, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.withAlpha(Theme.warning, 0.2)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: upgradeNotesColumn
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
ChangelogUpgradeNote {
|
||||||
|
width: parent.width
|
||||||
|
text: "Ghostty theme path changed to ~/.config/ghostty/themes/danktheme"
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogUpgradeNote {
|
||||||
|
width: parent.width
|
||||||
|
text: "VS Code theme reinstall required"
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogUpgradeNote {
|
||||||
|
width: parent.width
|
||||||
|
text: "Clipboard history migration available from cliphist"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "See full release notes for migration steps"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
quickshell/Modals/Changelog/ChangelogFeatureCard.qml
Normal file
78
quickshell/Modals/Changelog/ChangelogFeatureCard.qml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string iconName: ""
|
||||||
|
property string title: ""
|
||||||
|
property string description: ""
|
||||||
|
|
||||||
|
signal clicked
|
||||||
|
|
||||||
|
readonly property real iconContainerSize: Math.round(Theme.iconSize * 1.3)
|
||||||
|
|
||||||
|
height: Math.round(Theme.fontSizeMedium * 4.2)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: parent.radius
|
||||||
|
color: Theme.primary
|
||||||
|
opacity: mouseArea.containsMouse ? 0.12 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: root.iconContainerSize
|
||||||
|
height: root.iconContainerSize
|
||||||
|
radius: Math.round(root.iconContainerSize * 0.28)
|
||||||
|
color: Theme.primaryContainer
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: root.iconName
|
||||||
|
size: Theme.iconSize - 6
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
width: parent.width - root.iconContainerSize - Theme.spacingS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.title
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.description
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.clicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
155
quickshell/Modals/Changelog/ChangelogModal.qml
Normal file
155
quickshell/Modals/Changelog/ChangelogModal.qml
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
FloatingWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property int modalWidth: 680
|
||||||
|
readonly property int modalHeight: screen ? Math.min(720, screen.height - 80) : 720
|
||||||
|
|
||||||
|
signal changelogDismissed
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
objectName: "changelogModal"
|
||||||
|
title: "What's New"
|
||||||
|
minimumSize: Qt.size(modalWidth, modalHeight)
|
||||||
|
maximumSize: Qt.size(modalWidth, modalHeight)
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
id: contentFocusScope
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
Keys.onEscapePressed: event => {
|
||||||
|
root.dismiss();
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
root.dismiss();
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
height: headerRow.height + Theme.spacingM
|
||||||
|
onPressed: windowControls.tryStartMove()
|
||||||
|
onDoubleClicked: windowControls.tryToggleMaximize()
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
height: Math.round(Theme.fontSizeMedium * 2.85)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
visible: windowControls.supported && windowControls.canMaximize
|
||||||
|
iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: windowControls.tryToggleMaximize()
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
onClicked: root.dismiss()
|
||||||
|
|
||||||
|
DankTooltip {
|
||||||
|
text: "Close"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.bottom: footerRow.top
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
clip: true
|
||||||
|
contentHeight: mainColumn.height + Theme.spacingL * 2
|
||||||
|
contentWidth: width
|
||||||
|
|
||||||
|
ChangelogContent {
|
||||||
|
id: mainColumn
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: Math.min(600, parent.width - Theme.spacingXL * 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: footerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: Math.round(Theme.fontSizeMedium * 4.5)
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.top: parent.top
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Theme.outlineMedium
|
||||||
|
opacity: 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: "Read Full Release Notes"
|
||||||
|
iconName: "open_in_new"
|
||||||
|
backgroundColor: Theme.surfaceContainerHighest
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/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;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
quickshell/Modals/Changelog/ChangelogUpgradeNote.qml
Normal file
27
quickshell/Modals/Changelog/ChangelogUpgradeNote.qml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias text: noteText.text
|
||||||
|
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "arrow_right"
|
||||||
|
size: Theme.iconSizeSmall - 2
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: noteText
|
||||||
|
width: root.width - Theme.iconSizeSmall - Theme.spacingS
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user