mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-29 07:52:50 -05:00
Compare commits
67 Commits
972fc534a4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac509933d7 | ||
|
|
f49f98ff85 | ||
|
|
10923346d7 | ||
|
|
f27bffc387 | ||
|
|
36b43f93a3 | ||
|
|
2deeab9d08 | ||
|
|
f00854879c | ||
|
|
75fd62865b | ||
|
|
757054e140 | ||
|
|
eda59b348c | ||
|
|
d19e81ffac | ||
|
|
60c6872aec | ||
|
|
a9cb2fe912 | ||
|
|
a168a8160c | ||
|
|
78662f9613 | ||
|
|
d9d7bb8dcc | ||
|
|
3136f48b30 | ||
|
|
0c46711b01 | ||
|
|
68159b5c41 | ||
|
|
6557d66f94 | ||
|
|
9553cb06d3 | ||
|
|
122fb16dfb | ||
|
|
511502220f | ||
|
|
8bfe7439c0 | ||
|
|
8499033221 | ||
|
|
705d5b04dd | ||
|
|
17eaa761f8 | ||
|
|
1cdbd01748 | ||
|
|
08cc076a4c | ||
|
|
2a02d5594c | ||
|
|
2263338878 | ||
|
|
26bc5425d3 | ||
|
|
38b4d1dc95 | ||
|
|
3aaca7ff39 | ||
|
|
83d9808536 | ||
|
|
ad458dfece | ||
|
|
8f6fe7ed2b | ||
|
|
419a692593 | ||
|
|
03fdf795e0 | ||
|
|
832807a217 | ||
|
|
f7df3b2a68 | ||
|
|
0d03e73595 | ||
|
|
c5ae1a77d3 | ||
|
|
5f16624000 | ||
|
|
80025804ab | ||
|
|
028d3b4e61 | ||
|
|
9cce5ccfe6 | ||
|
|
a260b8060e | ||
|
|
f945307232 | ||
|
|
8f44d52cb2 | ||
|
|
3413cb7b89 | ||
|
|
4e3b24ffbb | ||
|
|
03cfa55e0b | ||
|
|
a887e60f40 | ||
|
|
816819bf9f | ||
|
|
78f3bb3812 | ||
|
|
01d7ed5dd8 | ||
|
|
50311db280 | ||
|
|
01b1a276c5 | ||
|
|
6d4c31492c | ||
|
|
f8c5f07e9f | ||
|
|
11e23feb0e | ||
|
|
b4ba2dac37 | ||
|
|
d013c3b718 | ||
|
|
b3ea28c5c4 | ||
|
|
775b381987 | ||
|
|
3a41f2f1ed |
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Suggest a new feature or improvement for DMS
|
description: Suggest a new feature or improvement for DMS. Keep features focused on a single topic with clear benefits, examples, etc. Avoid vague or broad requests, they will be closed.
|
||||||
labels:
|
labels:
|
||||||
- enhancement
|
- enhancement
|
||||||
body:
|
body:
|
||||||
|
|||||||
5
.github/workflows/prek.yml
vendored
5
.github/workflows/prek.yml
vendored
@@ -20,5 +20,10 @@ jobs:
|
|||||||
- name: Add a flatpak that mutagen could support
|
- name: Add a flatpak that mutagen could support
|
||||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: core/go.mod
|
||||||
|
|
||||||
- name: run pre-commit hooks
|
- name: run pre-commit hooks
|
||||||
uses: j178/prek-action@v1
|
uses: j178/prek-action@v1
|
||||||
|
|||||||
2
.github/workflows/run-obs.yml
vendored
2
.github/workflows/run-obs.yml
vendored
@@ -17,7 +17,7 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-updates:
|
check-updates:
|
||||||
|
|||||||
2
.github/workflows/run-ppa.yml
vendored
2
.github/workflows/run-ppa.yml
vendored
@@ -12,7 +12,7 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-updates:
|
check-updates:
|
||||||
|
|||||||
@@ -10,3 +10,11 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: shellcheck
|
- id: shellcheck
|
||||||
args: [-e, SC2164, -e, SC2001, -e, SC2012, -e, SC2317]
|
args: [-e, SC2164, -e, SC2001, -e, SC2012, -e, SC2317]
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: go-mod-tidy
|
||||||
|
name: go mod tidy
|
||||||
|
entry: bash -c 'cd core && go mod tidy'
|
||||||
|
language: system
|
||||||
|
files: ^core/.*\.(go|mod|sum)$
|
||||||
|
pass_filenames: false
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ This file is more of a quick reference so I know what to account for before next
|
|||||||
- dbus API for plugins, KDEConnect
|
- dbus API for plugins, KDEConnect
|
||||||
- new dank16 algorithm
|
- new dank16 algorithm
|
||||||
- launcher actions, customize env, args, name, icon
|
- launcher actions, customize env, args, name, icon
|
||||||
|
- launcher v2 - omega stuff, GIF search, supa powerful
|
||||||
|
- dock on bar
|
||||||
|
- window rule manager, with IPC - #TODO verify RTL layout (niri only)
|
||||||
|
|
||||||
# 1.2.0
|
# 1.2.0
|
||||||
|
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -58,10 +58,10 @@ install-completions:
|
|||||||
install-systemd:
|
install-systemd:
|
||||||
@echo "Installing systemd user service..."
|
@echo "Installing systemd user service..."
|
||||||
@mkdir -p $(SYSTEMD_USER_DIR)
|
@mkdir -p $(SYSTEMD_USER_DIR)
|
||||||
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR); fi
|
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR); fi
|
||||||
@sed 's|/usr/bin/dms|$(INSTALL_DIR)/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
|
@sed 's|/usr/bin/dms|$(INSTALL_DIR)/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
|
||||||
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
||||||
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR)/dms.service; fi
|
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR)/dms.service; fi
|
||||||
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
|
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
|
||||||
|
|
||||||
install-icon:
|
install-icon:
|
||||||
|
|||||||
@@ -12,17 +12,21 @@ import (
|
|||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
_ "golang.org/x/image/bmp"
|
_ "golang.org/x/image/bmp"
|
||||||
_ "golang.org/x/image/tiff"
|
_ "golang.org/x/image/tiff"
|
||||||
|
_ "golang.org/x/image/webp"
|
||||||
|
|
||||||
"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"
|
||||||
@@ -48,6 +52,7 @@ var (
|
|||||||
clipCopyForeground bool
|
clipCopyForeground bool
|
||||||
clipCopyPasteOnce bool
|
clipCopyPasteOnce bool
|
||||||
clipCopyType string
|
clipCopyType string
|
||||||
|
clipCopyDownload bool
|
||||||
clipJSONOutput bool
|
clipJSONOutput bool
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -184,11 +189,12 @@ 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")
|
||||||
clipCopyCmd.Flags().StringVarP(&clipCopyType, "type", "t", "text/plain;charset=utf-8", "MIME type")
|
clipCopyCmd.Flags().StringVarP(&clipCopyType, "type", "t", "text/plain;charset=utf-8", "MIME type")
|
||||||
|
clipCopyCmd.Flags().BoolVarP(&clipCopyDownload, "download", "d", false, "Download URL as image and copy as file")
|
||||||
|
|
||||||
clipWatchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
clipWatchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
||||||
clipHistoryCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
clipHistoryCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
||||||
clipGetCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
clipGetCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
||||||
clipGetCmd.Flags().BoolVarP(&clipGetCopy, "copy", "c", false, "Copy entry to clipboard")
|
clipGetCmd.Flags().BoolVarP(&clipGetCopy, "copy", "C", false, "Copy entry to clipboard")
|
||||||
|
|
||||||
clipSearchCmd.Flags().IntVarP(&clipSearchLimit, "limit", "l", 50, "Max results")
|
clipSearchCmd.Flags().IntVarP(&clipSearchLimit, "limit", "l", 50, "Max results")
|
||||||
clipSearchCmd.Flags().IntVarP(&clipSearchOffset, "offset", "o", 0, "Result offset")
|
clipSearchCmd.Flags().IntVarP(&clipSearchOffset, "offset", "o", 0, "Result offset")
|
||||||
@@ -215,9 +221,10 @@ func init() {
|
|||||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||||
var data []byte
|
var data []byte
|
||||||
|
|
||||||
if len(args) > 0 {
|
switch {
|
||||||
|
case len(args) > 0:
|
||||||
data = []byte(args[0])
|
data = []byte(args[0])
|
||||||
} else {
|
default:
|
||||||
var err error
|
var err error
|
||||||
data, err = io.ReadAll(os.Stdin)
|
data, err = io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -225,11 +232,68 @@ func runClipCopy(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if clipCopyDownload {
|
||||||
|
filePath, err := downloadToTempFile(strings.TrimSpace(string(data)))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("download: %v", err)
|
||||||
|
}
|
||||||
|
if err := copyFileToClipboard(filePath); err != nil {
|
||||||
|
log.Fatalf("copy file: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Downloaded and copied: %s\n", filePath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if clipCopyType == "__multi__" {
|
||||||
|
offers, err := parseMultiOffers(data)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("parse multi offers: %v", err)
|
||||||
|
}
|
||||||
|
if err := clipboard.CopyMulti(offers, true, clipCopyPasteOnce); err != nil {
|
||||||
|
log.Fatalf("copy multi: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
||||||
log.Fatalf("copy: %v", err)
|
log.Fatalf("copy: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseMultiOffers(data []byte) ([]clipboard.Offer, error) {
|
||||||
|
var offers []clipboard.Offer
|
||||||
|
pos := 0
|
||||||
|
|
||||||
|
for pos < len(data) {
|
||||||
|
mimeEnd := bytes.IndexByte(data[pos:], 0)
|
||||||
|
if mimeEnd == -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
mimeType := string(data[pos : pos+mimeEnd])
|
||||||
|
pos += mimeEnd + 1
|
||||||
|
|
||||||
|
lenEnd := bytes.IndexByte(data[pos:], 0)
|
||||||
|
if lenEnd == -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dataLen, err := strconv.Atoi(string(data[pos : pos+lenEnd]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse length: %w", err)
|
||||||
|
}
|
||||||
|
pos += lenEnd + 1
|
||||||
|
|
||||||
|
if pos+dataLen > len(data) {
|
||||||
|
return nil, fmt.Errorf("data truncated")
|
||||||
|
}
|
||||||
|
offerData := data[pos : pos+dataLen]
|
||||||
|
pos += dataLen
|
||||||
|
|
||||||
|
offers = append(offers, clipboard.Offer{MimeType: mimeType, Data: offerData})
|
||||||
|
}
|
||||||
|
|
||||||
|
return offers, nil
|
||||||
|
}
|
||||||
|
|
||||||
func runClipPaste(cmd *cobra.Command, args []string) {
|
func runClipPaste(cmd *cobra.Command, args []string) {
|
||||||
data, _, err := clipboard.Paste()
|
data, _, err := clipboard.Paste()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -386,16 +450,13 @@ func runClipGet(cmd *cobra.Command, args []string) {
|
|||||||
req := models.Request{
|
req := models.Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "clipboard.copyEntry",
|
Method: "clipboard.copyEntry",
|
||||||
Params: map[string]any{
|
Params: map[string]any{"id": id},
|
||||||
"id": id,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
resp, err := sendServerRequest(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to copy clipboard entry: %v", err)
|
log.Fatalf("Failed to copy clipboard entry: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.Error != "" {
|
if resp.Error != "" {
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
log.Fatalf("Error: %s", resp.Error)
|
||||||
}
|
}
|
||||||
@@ -672,7 +733,7 @@ func runClipExport(cmd *cobra.Command, args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(args[0], out, 0644); err != nil {
|
if err := os.WriteFile(args[0], out, 0o644); err != nil {
|
||||||
log.Fatalf("Failed to write file: %v", err)
|
log.Fatalf("Failed to write file: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Exported to %s\n", args[0])
|
fmt.Printf("Exported to %s\n", args[0])
|
||||||
@@ -727,7 +788,7 @@ func runClipMigrate(cmd *cobra.Command, args []string) {
|
|||||||
log.Fatalf("Cliphist db not found: %s", dbPath)
|
log.Fatalf("Cliphist db not found: %s", dbPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := bolt.Open(dbPath, 0644, &bolt.Options{
|
db, err := bolt.Open(dbPath, 0o644, &bolt.Options{
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
Timeout: 1 * time.Second,
|
Timeout: 1 * time.Second,
|
||||||
})
|
})
|
||||||
@@ -795,3 +856,113 @@ func detectMimeType(data []byte) string {
|
|||||||
func btoi(v []byte) uint64 {
|
func btoi(v []byte) uint64 {
|
||||||
return binary.BigEndian.Uint64(v)
|
return binary.BigEndian.Uint64(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func downloadToTempFile(rawURL string) (string, error) {
|
||||||
|
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
|
||||||
|
return "", fmt.Errorf("invalid URL: %s", rawURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parse URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := filepath.Ext(parsedURL.Path)
|
||||||
|
if ext == "" {
|
||||||
|
ext = ".png"
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var contentType string
|
||||||
|
var lastErr error
|
||||||
|
|
||||||
|
for attempt := 0; attempt < 3; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
time.Sleep(time.Duration(attempt) * 500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", rawURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = fmt.Errorf("create request: %w", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||||
|
req.Header.Set("Accept", "image/*,video/*,*/*")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = fmt.Errorf("download (attempt %d): %w", attempt+1, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
resp.Body.Close()
|
||||||
|
lastErr = fmt.Errorf("download failed (attempt %d): status %d", attempt+1, resp.StatusCode)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err = io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
lastErr = fmt.Errorf("read response (attempt %d): %w", attempt+1, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType = resp.Header.Get("Content-Type")
|
||||||
|
if idx := strings.Index(contentType, ";"); idx != -1 {
|
||||||
|
contentType = strings.TrimSpace(contentType[:idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
lastErr = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastErr != nil {
|
||||||
|
return "", lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return "", fmt.Errorf("downloaded empty file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(contentType, "image/") && !strings.HasPrefix(contentType, "video/") {
|
||||||
|
if _, _, err := image.DecodeConfig(bytes.NewReader(data)); err != nil {
|
||||||
|
return "", fmt.Errorf("not a valid media file (content-type: %s)", contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir, err := os.UserCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
cacheDir = "/tmp"
|
||||||
|
}
|
||||||
|
clipDir := filepath.Join(cacheDir, "dms", "clipboard")
|
||||||
|
if err := os.MkdirAll(clipDir, 0o755); err != nil {
|
||||||
|
return "", fmt.Errorf("create cache dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(clipDir, fmt.Sprintf("%d%s", time.Now().UnixNano(), ext))
|
||||||
|
if err := os.WriteFile(filePath, data, 0o644); err != nil {
|
||||||
|
return "", fmt.Errorf("write file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFileToClipboard(filePath string) error {
|
||||||
|
req := models.Request{
|
||||||
|
ID: 1,
|
||||||
|
Method: "clipboard.copyFile",
|
||||||
|
Params: map[string]any{"filePath": filePath},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := sendServerRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("server request: %w", err)
|
||||||
|
}
|
||||||
|
if resp.Error != "" {
|
||||||
|
return fmt.Errorf("server error: %s", resp.Error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -591,7 +591,7 @@ ShellRoot {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(testScript, []byte(qmlContent), 0644); err != nil {
|
if err := os.WriteFile(testScript, []byte(qmlContent), 0o644); err != nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -752,7 +752,7 @@ func checkConfigurationFiles() []checkResult {
|
|||||||
|
|
||||||
status := statusOK
|
status := statusOK
|
||||||
message := "Present"
|
message := "Present"
|
||||||
if info.Mode().Perm()&0200 == 0 {
|
if info.Mode().Perm()&0o200 == 0 {
|
||||||
status = statusWarn
|
status = statusWarn
|
||||||
message += " (read-only)"
|
message += " (read-only)"
|
||||||
}
|
}
|
||||||
|
|||||||
336
core/cmd/dms/commands_windowrules.go
Normal file
336
core/cmd/dms/commands_windowrules.go
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules/providers"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var windowrulesCmd = &cobra.Command{
|
||||||
|
Use: "windowrules",
|
||||||
|
Short: "Manage window rules",
|
||||||
|
}
|
||||||
|
|
||||||
|
var windowrulesListCmd = &cobra.Command{
|
||||||
|
Use: "list [compositor]",
|
||||||
|
Short: "List all window rules",
|
||||||
|
Long: "List all window rules from compositor config file. Returns JSON with rules and DMS status.",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runWindowrulesList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var windowrulesAddCmd = &cobra.Command{
|
||||||
|
Use: "add <compositor> '<json>'",
|
||||||
|
Short: "Add a window rule to DMS file",
|
||||||
|
Long: "Add a new window rule to the DMS-managed rules file.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runWindowrulesAdd,
|
||||||
|
}
|
||||||
|
|
||||||
|
var windowrulesUpdateCmd = &cobra.Command{
|
||||||
|
Use: "update <compositor> <id> '<json>'",
|
||||||
|
Short: "Update a window rule in DMS file",
|
||||||
|
Long: "Update an existing window rule in the DMS-managed rules file.",
|
||||||
|
Args: cobra.ExactArgs(3),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runWindowrulesUpdate,
|
||||||
|
}
|
||||||
|
|
||||||
|
var windowrulesRemoveCmd = &cobra.Command{
|
||||||
|
Use: "remove <compositor> <id>",
|
||||||
|
Short: "Remove a window rule from DMS file",
|
||||||
|
Long: "Remove a window rule from the DMS-managed rules file.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runWindowrulesRemove,
|
||||||
|
}
|
||||||
|
|
||||||
|
var windowrulesReorderCmd = &cobra.Command{
|
||||||
|
Use: "reorder <compositor> '<json-array-of-ids>'",
|
||||||
|
Short: "Reorder window rules in DMS file",
|
||||||
|
Long: "Reorder window rules by providing a JSON array of rule IDs in the desired order.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runWindowrulesReorder,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
configCmd.AddCommand(windowrulesCmd)
|
||||||
|
windowrulesCmd.AddCommand(windowrulesListCmd)
|
||||||
|
windowrulesCmd.AddCommand(windowrulesAddCmd)
|
||||||
|
windowrulesCmd.AddCommand(windowrulesUpdateCmd)
|
||||||
|
windowrulesCmd.AddCommand(windowrulesRemoveCmd)
|
||||||
|
windowrulesCmd.AddCommand(windowrulesReorderCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowRulesListResult struct {
|
||||||
|
Rules []windowrules.WindowRule `json:"rules"`
|
||||||
|
DMSStatus *windowrules.DMSRulesStatus `json:"dmsStatus,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowRuleWriteResult struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCompositor(args []string) string {
|
||||||
|
if len(args) > 0 {
|
||||||
|
return strings.ToLower(args[0])
|
||||||
|
}
|
||||||
|
if os.Getenv("NIRI_SOCKET") != "" {
|
||||||
|
return "niri"
|
||||||
|
}
|
||||||
|
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
||||||
|
return "hyprland"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRuleError(errMsg string) {
|
||||||
|
result := WindowRuleWriteResult{Success: false, Error: errMsg}
|
||||||
|
output, _ := json.Marshal(result)
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRuleSuccess(id, path string) {
|
||||||
|
result := WindowRuleWriteResult{Success: true, ID: id, Path: path}
|
||||||
|
output, _ := json.Marshal(result)
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWindowrulesList(cmd *cobra.Command, args []string) {
|
||||||
|
compositor := getCompositor(args)
|
||||||
|
if compositor == "" {
|
||||||
|
log.Fatalf("Could not detect compositor. Please specify: hyprland or niri")
|
||||||
|
}
|
||||||
|
|
||||||
|
var result WindowRulesListResult
|
||||||
|
|
||||||
|
switch compositor {
|
||||||
|
case "niri":
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to expand niri config path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parseResult, err := providers.ParseNiriWindowRules(configDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to parse niri window rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allRules := providers.ConvertNiriRulesToWindowRules(parseResult.Rules)
|
||||||
|
|
||||||
|
provider := providers.NewNiriWritableProvider(configDir)
|
||||||
|
dmsRulesPath := provider.GetOverridePath()
|
||||||
|
dmsRules, _ := provider.LoadDMSRules()
|
||||||
|
|
||||||
|
dmsRuleMap := make(map[int]windowrules.WindowRule)
|
||||||
|
for i, dr := range dmsRules {
|
||||||
|
dmsRuleMap[i] = dr
|
||||||
|
}
|
||||||
|
|
||||||
|
dmsIdx := 0
|
||||||
|
for i, r := range allRules {
|
||||||
|
if r.Source == dmsRulesPath {
|
||||||
|
if dmr, ok := dmsRuleMap[dmsIdx]; ok {
|
||||||
|
allRules[i].ID = dmr.ID
|
||||||
|
allRules[i].Name = dmr.Name
|
||||||
|
}
|
||||||
|
dmsIdx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Rules = allRules
|
||||||
|
result.DMSStatus = parseResult.DMSStatus
|
||||||
|
|
||||||
|
case "hyprland":
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to expand hyprland config path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parseResult, err := providers.ParseHyprlandWindowRules(configDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to parse hyprland window rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allRules := providers.ConvertHyprlandRulesToWindowRules(parseResult.Rules)
|
||||||
|
|
||||||
|
provider := providers.NewHyprlandWritableProvider(configDir)
|
||||||
|
dmsRulesPath := provider.GetOverridePath()
|
||||||
|
dmsRules, _ := provider.LoadDMSRules()
|
||||||
|
|
||||||
|
dmsRuleMap := make(map[int]windowrules.WindowRule)
|
||||||
|
for i, dr := range dmsRules {
|
||||||
|
dmsRuleMap[i] = dr
|
||||||
|
}
|
||||||
|
|
||||||
|
dmsIdx := 0
|
||||||
|
for i, r := range allRules {
|
||||||
|
if r.Source == dmsRulesPath {
|
||||||
|
if dmr, ok := dmsRuleMap[dmsIdx]; ok {
|
||||||
|
allRules[i].ID = dmr.ID
|
||||||
|
allRules[i].Name = dmr.Name
|
||||||
|
}
|
||||||
|
dmsIdx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Rules = allRules
|
||||||
|
result.DMSStatus = parseResult.DMSStatus
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown compositor: %s", compositor)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := json.Marshal(result)
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWindowrulesAdd(cmd *cobra.Command, args []string) {
|
||||||
|
compositor := strings.ToLower(args[0])
|
||||||
|
ruleJSON := args[1]
|
||||||
|
|
||||||
|
var rule windowrules.WindowRule
|
||||||
|
if err := json.Unmarshal([]byte(ruleJSON), &rule); err != nil {
|
||||||
|
writeRuleError(fmt.Sprintf("Invalid JSON: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.ID == "" {
|
||||||
|
rule.ID = generateRuleID()
|
||||||
|
}
|
||||||
|
rule.Enabled = true
|
||||||
|
|
||||||
|
provider := getWindowRulesProvider(compositor)
|
||||||
|
if provider == nil {
|
||||||
|
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := provider.SetRule(rule); err != nil {
|
||||||
|
writeRuleError(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
writeRuleSuccess(rule.ID, provider.GetOverridePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWindowrulesUpdate(cmd *cobra.Command, args []string) {
|
||||||
|
compositor := strings.ToLower(args[0])
|
||||||
|
ruleID := args[1]
|
||||||
|
ruleJSON := args[2]
|
||||||
|
|
||||||
|
var rule windowrules.WindowRule
|
||||||
|
if err := json.Unmarshal([]byte(ruleJSON), &rule); err != nil {
|
||||||
|
writeRuleError(fmt.Sprintf("Invalid JSON: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.ID = ruleID
|
||||||
|
|
||||||
|
provider := getWindowRulesProvider(compositor)
|
||||||
|
if provider == nil {
|
||||||
|
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := provider.SetRule(rule); err != nil {
|
||||||
|
writeRuleError(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
writeRuleSuccess(rule.ID, provider.GetOverridePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWindowrulesRemove(cmd *cobra.Command, args []string) {
|
||||||
|
compositor := strings.ToLower(args[0])
|
||||||
|
ruleID := args[1]
|
||||||
|
|
||||||
|
provider := getWindowRulesProvider(compositor)
|
||||||
|
if provider == nil {
|
||||||
|
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := provider.RemoveRule(ruleID); err != nil {
|
||||||
|
writeRuleError(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
writeRuleSuccess(ruleID, provider.GetOverridePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWindowrulesReorder(cmd *cobra.Command, args []string) {
|
||||||
|
compositor := strings.ToLower(args[0])
|
||||||
|
idsJSON := args[1]
|
||||||
|
|
||||||
|
var ids []string
|
||||||
|
if err := json.Unmarshal([]byte(idsJSON), &ids); err != nil {
|
||||||
|
writeRuleError(fmt.Sprintf("Invalid JSON array: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := getWindowRulesProvider(compositor)
|
||||||
|
if provider == nil {
|
||||||
|
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := provider.ReorderRules(ids); err != nil {
|
||||||
|
writeRuleError(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
writeRuleSuccess("", provider.GetOverridePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWindowRulesProvider(compositor string) windowrules.WritableProvider {
|
||||||
|
switch compositor {
|
||||||
|
case "niri":
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return providers.NewNiriWritableProvider(configDir)
|
||||||
|
case "hyprland":
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return providers.NewHyprlandWritableProvider(configDir)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRuleID() string {
|
||||||
|
return fmt.Sprintf("wr_%d", time.Now().UnixNano())
|
||||||
|
}
|
||||||
12
core/go.mod
12
core/go.mod
@@ -4,7 +4,7 @@ go 1.24.6
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||||
github.com/alecthomas/chroma/v2 v2.17.2
|
github.com/alecthomas/chroma/v2 v2.23.1
|
||||||
github.com/charmbracelet/bubbles v0.21.0
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
@@ -13,7 +13,7 @@ require (
|
|||||||
github.com/godbus/dbus/v5 v5.2.2
|
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-20251203232544-981d4ecc17c3
|
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/yuin/goldmark v1.7.16
|
github.com/yuin/goldmark v1.7.16
|
||||||
@@ -26,10 +26,10 @@ require (
|
|||||||
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.7.0 // indirect
|
github.com/clipperhouse/displaywidth v0.8.0 // 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.4.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.2 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
@@ -56,7 +56,7 @@ require (
|
|||||||
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-20260114124804-a8db3a6585a6
|
github.com/go-git/go-git/v6 v6.0.0-20260123133532-f99a98e81ce9
|
||||||
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
|
||||||
|
|||||||
32
core/go.sum
32
core/go.sum
@@ -7,11 +7,11 @@ github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3np
|
|||||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||||
github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI=
|
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||||
github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
@@ -38,14 +38,14 @@ github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaL
|
|||||||
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.7.0 h1:QNv1GYsnLX9QBrcWUtMlogpTXuM5FVnBwKWp1O5NwmE=
|
github.com/clipperhouse/displaywidth v0.8.0 h1:/z8v+H+4XLluJKS7rAc7uHZTalT5Z+1430ld3lePSRI=
|
||||||
github.com/clipperhouse/displaywidth v0.7.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
github.com/clipperhouse/displaywidth v0.8.0/go.mod h1:UpOXiIKep+TohQYwvAAM/VDU8v3Z5rnWTxiwueR0XvQ=
|
||||||
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.4.0 h1:RXqE/l5EiAbA4u97giimKNlmpvkmz+GrBVTelsoXy9g=
|
||||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
github.com/clipperhouse/uax29/v2 v2.4.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
github.com/cloudflare/circl v1.6.3/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=
|
||||||
@@ -68,10 +68,10 @@ 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-20260114122816-19306b749ecc h1:rhkjrnRkamkRC7woapp425E4CAH6RPcqsS9X8LA93IY=
|
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc h1:rhkjrnRkamkRC7woapp425E4CAH6RPcqsS9X8LA93IY=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc=
|
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc=
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146 h1:xYfxAopYyL44ot6dMBIb1Z1njFM0ZBQ99HdIB99KxLs=
|
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII=
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146/go.mod h1:QE/75B8tBSLNGyUUbA9tw3EGHoFtYOtypa2h8YJxsWI=
|
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6 h1:Yo1MlE8LpvD0pr7mZ04b6hKZKQcPvLrQFgyY1jNMEyU=
|
github.com/go-git/go-git/v6 v6.0.0-20260123133532-f99a98e81ce9 h1:VzdR70t+SMjYnBgnbtNpq4ElZAAovLPMG+GFX8OBRtM=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6/go.mod h1:enMzPHv+9hL4B7tH7OJGQKNzCkMzXovUoaiXfsLF7Xs=
|
github.com/go-git/go-git/v6 v6.0.0-20260123133532-f99a98e81ce9/go.mod h1:EWlxLBkiFCzXNCadvt05fT9PCAE2sUedgDsvUUIo18s=
|
||||||
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=
|
||||||
@@ -124,8 +124,8 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
|
|||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
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-20251203232544-981d4ecc17c3 h1:msKaIZrrNpvofLPDzNBW3152PJBsnPZsoNNosOCS+C0=
|
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6 h1:JsjzqC6ymELkN4XlTjZPSahSAem21GySugLbKz6uF5E=
|
||||||
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6/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=
|
||||||
|
|||||||
@@ -330,3 +330,163 @@ func selectPreferredMimeType(mimes []string) string {
|
|||||||
func IsImageMimeType(mime string) bool {
|
func IsImageMimeType(mime string) bool {
|
||||||
return len(mime) > 6 && mime[:6] == "image/"
|
return len(mime) > 6 && mime[:6] == "image/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Offer struct {
|
||||||
|
MimeType string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
|
||||||
|
if !foreground {
|
||||||
|
return copyMultiFork(offers, pasteOnce)
|
||||||
|
}
|
||||||
|
return copyMultiServe(offers, pasteOnce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyMultiFork(offers []Offer, pasteOnce bool) error {
|
||||||
|
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
|
||||||
|
if pasteOnce {
|
||||||
|
args = append(args, "--paste-once")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Stdin = nil
|
||||||
|
cmd.Stdout = nil
|
||||||
|
cmd.Stderr = nil
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, offer := range offers {
|
||||||
|
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
|
||||||
|
if _, err := stdin.Write(offer.Data); err != nil {
|
||||||
|
stdin.Close()
|
||||||
|
return fmt.Errorf("write offer data: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdin.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
||||||
|
display, err := wlclient.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wayland connect: %w", err)
|
||||||
|
}
|
||||||
|
defer display.Destroy()
|
||||||
|
|
||||||
|
ctx := display.Context()
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get registry: %w", err)
|
||||||
|
}
|
||||||
|
defer registry.Destroy()
|
||||||
|
|
||||||
|
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||||
|
var seat *wlclient.Seat
|
||||||
|
var bindErr error
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case "ext_data_control_manager_v1":
|
||||||
|
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
||||||
|
bindErr = err
|
||||||
|
}
|
||||||
|
case "wl_seat":
|
||||||
|
if seat != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seat = wlclient.NewSeat(ctx)
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
||||||
|
bindErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
display.Roundtrip()
|
||||||
|
display.Roundtrip()
|
||||||
|
|
||||||
|
if bindErr != nil {
|
||||||
|
return fmt.Errorf("registry bind: %w", bindErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dataControlMgr == nil {
|
||||||
|
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
|
}
|
||||||
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
|
if seat == nil {
|
||||||
|
return fmt.Errorf("no seat available")
|
||||||
|
}
|
||||||
|
|
||||||
|
device, err := dataControlMgr.GetDataDevice(seat)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get data device: %w", err)
|
||||||
|
}
|
||||||
|
defer device.Destroy()
|
||||||
|
|
||||||
|
source, err := dataControlMgr.CreateDataSource()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create data source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
offerMap := make(map[string][]byte)
|
||||||
|
for _, offer := range offers {
|
||||||
|
if err := source.Offer(offer.MimeType); err != nil {
|
||||||
|
return fmt.Errorf("offer %s: %w", offer.MimeType, err)
|
||||||
|
}
|
||||||
|
offerMap[offer.MimeType] = offer.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelled := make(chan struct{})
|
||||||
|
pasted := make(chan struct{}, 1)
|
||||||
|
|
||||||
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
|
defer syscall.Close(e.Fd)
|
||||||
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if data, ok := offerMap[e.MimeType]; ok {
|
||||||
|
file.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case pasted <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
||||||
|
close(cancelled)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := device.SetSelection(source); err != nil {
|
||||||
|
return fmt.Errorf("set selection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
display.Roundtrip()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-cancelled:
|
||||||
|
return nil
|
||||||
|
case <-pasted:
|
||||||
|
if pasteOnce {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if err := ctx.Dispatch(); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
|||||||
return fmt.Errorf("get db path: %w", err)
|
return fmt.Errorf("get db path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := bolt.Open(dbPath, 0644, &bolt.Options{Timeout: 1 * time.Second})
|
db, err := bolt.Open(dbPath, 0o644, &bolt.Options{Timeout: 1 * time.Second})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("open db: %w", err)
|
return fmt.Errorf("open db: %w", err)
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@ func GetDBPath() (string, error) {
|
|||||||
oldPath := filepath.Join(oldDir, "db")
|
oldPath := filepath.Join(oldDir, "db")
|
||||||
|
|
||||||
if _, err := os.Stat(oldPath); err == nil {
|
if _, err := os.Stat(oldPath); err == nil {
|
||||||
if err := os.MkdirAll(newDir, 0700); err != nil {
|
if err := os.MkdirAll(newDir, 0o700); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if err := os.Rename(oldPath, newPath); err != nil {
|
if err := os.Rename(oldPath, newPath); err != nil {
|
||||||
@@ -142,7 +142,7 @@ func GetDBPath() (string, error) {
|
|||||||
return newPath, nil
|
return newPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(newDir, 0700); err != nil {
|
if err := os.MkdirAll(newDir, 0o700); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return newPath, nil
|
return newPath, nil
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package clipboard
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -130,13 +131,29 @@ func Watch(ctx context.Context, callback func(data []byte, mimeType string)) err
|
|||||||
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
||||||
return fmt.Errorf("set read deadline: %w", err)
|
return fmt.Errorf("set read deadline: %w", err)
|
||||||
}
|
}
|
||||||
if err := wlCtx.Dispatch(); err != nil && err != os.ErrDeadlineExceeded {
|
if err := wlCtx.Dispatch(); err != nil {
|
||||||
|
if isTimeoutError(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
return fmt.Errorf("dispatch: %w", err)
|
return fmt.Errorf("dispatch: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isTimeoutError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func WatchChan(ctx context.Context) (<-chan ClipboardChange, <-chan error) {
|
func WatchChan(ctx context.Context) (<-chan ClipboardChange, <-chan error) {
|
||||||
ch := make(chan ClipboardChange, 16)
|
ch := make(chan ClipboardChange, 16)
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
|
|||||||
@@ -126,13 +126,13 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(result.Path)
|
configDir := filepath.Dir(result.Path)
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
dmsDir := filepath.Join(configDir, "dms")
|
dmsDir := filepath.Join(configDir, "dms")
|
||||||
if err := os.MkdirAll(dmsDir, 0755); err != nil {
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
|
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
|
|||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
result.BackupPath = result.Path + ".backup." + timestamp
|
result.BackupPath = result.Path + ".backup." + timestamp
|
||||||
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
|
if err := os.WriteFile(result.BackupPath, existingData, 0o644); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
|
if err := os.WriteFile(result.Path, []byte(newConfig), 0o644); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to write config: %w", err)
|
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -211,6 +211,7 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
|||||||
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||||
{"outputs.kdl", ""},
|
{"outputs.kdl", ""},
|
||||||
{"cursor.kdl", ""},
|
{"cursor.kdl", ""},
|
||||||
|
{"windowrules.kdl", ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
@@ -220,7 +221,7 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
|||||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
|
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
||||||
@@ -238,7 +239,7 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(mainResult.Path)
|
configDir := filepath.Dir(mainResult.Path)
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -254,14 +255,14 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
|||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||||
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
if err := os.WriteFile(mainResult.BackupPath, existingData, 0o644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(mainResult.Path, []byte(GhosttyConfig), 0644); err != nil {
|
if err := os.WriteFile(mainResult.Path, []byte(GhosttyConfig), 0o644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -276,12 +277,12 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
themesDir := filepath.Dir(colorResult.Path)
|
themesDir := filepath.Dir(colorResult.Path)
|
||||||
if err := os.MkdirAll(themesDir, 0755); err != nil {
|
if err := os.MkdirAll(themesDir, 0o755); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create themes directory: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create themes directory: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil {
|
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0o644); err != nil {
|
||||||
colorResult.Error = fmt.Errorf("failed to write color config: %w", err)
|
colorResult.Error = fmt.Errorf("failed to write color config: %w", err)
|
||||||
return results, colorResult.Error
|
return results, colorResult.Error
|
||||||
}
|
}
|
||||||
@@ -302,7 +303,7 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(mainResult.Path)
|
configDir := filepath.Dir(mainResult.Path)
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -318,14 +319,14 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
|||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||||
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
if err := os.WriteFile(mainResult.BackupPath, existingData, 0o644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(mainResult.Path, []byte(KittyConfig), 0644); err != nil {
|
if err := os.WriteFile(mainResult.Path, []byte(KittyConfig), 0o644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -339,7 +340,7 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
|||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-theme.conf"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-theme.conf"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(themeResult.Path, []byte(KittyThemeConfig), 0644); err != nil {
|
if err := os.WriteFile(themeResult.Path, []byte(KittyThemeConfig), 0o644); err != nil {
|
||||||
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
||||||
return results, themeResult.Error
|
return results, themeResult.Error
|
||||||
}
|
}
|
||||||
@@ -353,7 +354,7 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
|||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-tabs.conf"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-tabs.conf"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(tabsResult.Path, []byte(KittyTabsConfig), 0644); err != nil {
|
if err := os.WriteFile(tabsResult.Path, []byte(KittyTabsConfig), 0o644); err != nil {
|
||||||
tabsResult.Error = fmt.Errorf("failed to write tabs config: %w", err)
|
tabsResult.Error = fmt.Errorf("failed to write tabs config: %w", err)
|
||||||
return results, tabsResult.Error
|
return results, tabsResult.Error
|
||||||
}
|
}
|
||||||
@@ -374,7 +375,7 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(mainResult.Path)
|
configDir := filepath.Dir(mainResult.Path)
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -390,14 +391,14 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
|||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||||
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
if err := os.WriteFile(mainResult.BackupPath, existingData, 0o644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(mainResult.Path, []byte(AlacrittyConfig), 0644); err != nil {
|
if err := os.WriteFile(mainResult.Path, []byte(AlacrittyConfig), 0o644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -411,7 +412,7 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
|||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "dank-theme.toml"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "dank-theme.toml"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(themeResult.Path, []byte(AlacrittyThemeConfig), 0644); err != nil {
|
if err := os.WriteFile(themeResult.Path, []byte(AlacrittyThemeConfig), 0o644); err != nil {
|
||||||
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
||||||
return results, themeResult.Error
|
return results, themeResult.Error
|
||||||
}
|
}
|
||||||
@@ -438,7 +439,7 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dms
|
|||||||
outputsContent.WriteString(output)
|
outputsContent.WriteString(output)
|
||||||
outputsContent.WriteString("\n\n")
|
outputsContent.WriteString("\n\n")
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0644); err != nil {
|
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0o644); err != nil {
|
||||||
cd.log(fmt.Sprintf("Warning: Failed to migrate outputs to %s: %v", outputsPath, err))
|
cd.log(fmt.Sprintf("Warning: Failed to migrate outputs to %s: %v", outputsPath, err))
|
||||||
} else {
|
} else {
|
||||||
cd.log("Migrated output sections to dms/outputs.kdl")
|
cd.log("Migrated output sections to dms/outputs.kdl")
|
||||||
@@ -479,13 +480,13 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(result.Path)
|
configDir := filepath.Dir(result.Path)
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
dmsDir := filepath.Join(configDir, "dms")
|
dmsDir := filepath.Join(configDir, "dms")
|
||||||
if err := os.MkdirAll(dmsDir, 0755); err != nil {
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
|
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -503,7 +504,7 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
result.BackupPath = result.Path + ".backup." + timestamp
|
result.BackupPath = result.Path + ".backup." + timestamp
|
||||||
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
|
if err := os.WriteFile(result.BackupPath, existingData, 0o644); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -538,7 +539,7 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
|
if err := os.WriteFile(result.Path, []byte(newConfig), 0o644); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to write config: %w", err)
|
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -563,6 +564,7 @@ func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalComman
|
|||||||
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||||
{"outputs.conf", ""},
|
{"outputs.conf", ""},
|
||||||
{"cursor.conf", ""},
|
{"cursor.conf", ""},
|
||||||
|
{"windowrules.conf", ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
@@ -572,7 +574,7 @@ func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalComman
|
|||||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
|
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
||||||
@@ -596,7 +598,7 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
|
|||||||
outputsContent.WriteString(monitor)
|
outputsContent.WriteString(monitor)
|
||||||
outputsContent.WriteString("\n")
|
outputsContent.WriteString("\n")
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0644); err != nil {
|
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0o644); err != nil {
|
||||||
cd.log(fmt.Sprintf("Warning: Failed to migrate monitors to %s: %v", outputsPath, err))
|
cd.log(fmt.Sprintf("Warning: Failed to migrate monitors to %s: %v", outputsPath, err))
|
||||||
} else {
|
} else {
|
||||||
cd.log("Migrated monitor sections to dms/outputs.conf")
|
cd.log("Migrated monitor sections to dms/outputs.conf")
|
||||||
|
|||||||
@@ -220,9 +220,9 @@ func TestConfigDeploymentFlow(t *testing.T) {
|
|||||||
t.Run("deploy ghostty config with existing file", func(t *testing.T) {
|
t.Run("deploy ghostty config with existing file", func(t *testing.T) {
|
||||||
existingContent := "# Old config\nfont-size = 14\n"
|
existingContent := "# Old config\nfont-size = 14\n"
|
||||||
ghosttyPath := getGhosttyPath()
|
ghosttyPath := getGhosttyPath()
|
||||||
err := os.MkdirAll(filepath.Dir(ghosttyPath), 0755)
|
err := os.MkdirAll(filepath.Dir(ghosttyPath), 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = os.WriteFile(ghosttyPath, []byte(existingContent), 0644)
|
err = os.WriteFile(ghosttyPath, []byte(existingContent), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results, err := cd.deployGhosttyConfig()
|
results, err := cd.deployGhosttyConfig()
|
||||||
@@ -422,9 +422,9 @@ general {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
hyprPath := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
hyprPath := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
||||||
err := os.MkdirAll(filepath.Dir(hyprPath), 0755)
|
err := os.MkdirAll(filepath.Dir(hyprPath), 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
|
err = os.WriteFile(hyprPath, []byte(existingContent), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
||||||
@@ -600,9 +600,9 @@ func TestAlacrittyConfigDeployment(t *testing.T) {
|
|||||||
t.Run("deploy alacritty config with existing file", func(t *testing.T) {
|
t.Run("deploy alacritty config with existing file", func(t *testing.T) {
|
||||||
existingContent := "# Old alacritty config\n[window]\nopacity = 0.9\n"
|
existingContent := "# Old alacritty config\n[window]\nopacity = 0.9\n"
|
||||||
alacrittyPath := filepath.Join(tempDir, ".config", "alacritty", "alacritty.toml")
|
alacrittyPath := filepath.Join(tempDir, ".config", "alacritty", "alacritty.toml")
|
||||||
err := os.MkdirAll(filepath.Dir(alacrittyPath), 0755)
|
err := os.MkdirAll(filepath.Dir(alacrittyPath), 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = os.WriteFile(alacrittyPath, []byte(existingContent), 0644)
|
err = os.WriteFile(alacrittyPath, []byte(existingContent), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results, err := cd.deployAlacrittyConfig()
|
results, err := cd.deployAlacrittyConfig()
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ bind = SUPER, F, fullscreen, 1
|
|||||||
bind = SUPER SHIFT, F, fullscreen, 0
|
bind = SUPER SHIFT, F, fullscreen, 0
|
||||||
bind = SUPER SHIFT, T, togglefloating
|
bind = SUPER SHIFT, T, togglefloating
|
||||||
bind = SUPER, W, togglegroup
|
bind = SUPER, W, togglegroup
|
||||||
|
bind = SUPER SHIFT, W, exec, dms ipc call window-rules toggle
|
||||||
|
|
||||||
# === Focus Navigation ===
|
# === Focus Navigation ===
|
||||||
bind = SUPER, left, movefocus, l
|
bind = SUPER, left, movefocus, l
|
||||||
@@ -91,6 +92,9 @@ bind = SUPER CTRL, up, movetoworkspace, e-1
|
|||||||
bind = SUPER CTRL, U, movetoworkspace, e+1
|
bind = SUPER CTRL, U, movetoworkspace, e+1
|
||||||
bind = SUPER CTRL, I, movetoworkspace, e-1
|
bind = SUPER CTRL, I, movetoworkspace, e-1
|
||||||
|
|
||||||
|
# === Workspace Management ===
|
||||||
|
bind = CTRL SHIFT, R, exec, dms ipc call workspace-rename open
|
||||||
|
|
||||||
# === Move Workspaces ===
|
# === Move Workspaces ===
|
||||||
bind = SUPER SHIFT, Page_Down, movetoworkspace, e+1
|
bind = SUPER SHIFT, Page_Down, movetoworkspace, e+1
|
||||||
bind = SUPER SHIFT, Page_Up, movetoworkspace, e-1
|
bind = SUPER SHIFT, Page_Up, movetoworkspace, e-1
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ master {
|
|||||||
misc {
|
misc {
|
||||||
disable_hyprland_logo = true
|
disable_hyprland_logo = true
|
||||||
disable_splash_rendering = true
|
disable_splash_rendering = true
|
||||||
vrr = 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==================
|
# ==================
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ binds {
|
|||||||
Mod+Shift+T { toggle-window-floating; }
|
Mod+Shift+T { toggle-window-floating; }
|
||||||
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
|
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
|
||||||
Mod+W { toggle-column-tabbed-display; }
|
Mod+W { toggle-column-tabbed-display; }
|
||||||
|
Mod+Shift+W hotkey-overlay-title="Create window rule" { spawn "dms" "ipc" "call" "window-rules" "toggle"; }
|
||||||
|
|
||||||
// === Focus Navigation ===
|
// === Focus Navigation ===
|
||||||
Mod+Left { focus-column-left; }
|
Mod+Left { focus-column-left; }
|
||||||
@@ -133,6 +134,11 @@ binds {
|
|||||||
Mod+Ctrl+U { move-column-to-workspace-down; }
|
Mod+Ctrl+U { move-column-to-workspace-down; }
|
||||||
Mod+Ctrl+I { move-column-to-workspace-up; }
|
Mod+Ctrl+I { move-column-to-workspace-up; }
|
||||||
|
|
||||||
|
// === Workspace Management ===
|
||||||
|
Ctrl+Shift+R hotkey-overlay-title="Rename Workspace" {
|
||||||
|
spawn "dms" "ipc" "call" "workspace-rename" "open";
|
||||||
|
}
|
||||||
|
|
||||||
// === Move Workspaces ===
|
// === Move Workspaces ===
|
||||||
Mod+Shift+Page_Down { move-workspace-down; }
|
Mod+Shift+Page_Down { move-workspace-down; }
|
||||||
Mod+Shift+Page_Up { move-workspace-up; }
|
Mod+Shift+Page_Up { move-workspace-up; }
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ func init() {
|
|||||||
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewArchDistribution(config, logChan)
|
return NewArchDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
|
Register("XeroLinux", "#888fe2", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewArchDistribution(config, logChan)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArchDistribution struct {
|
type ArchDistribution struct {
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ func (b *BaseDistribution) WriteEnvironmentConfig(terminal deps.Terminal) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
envDir := filepath.Join(homeDir, ".config", "environment.d")
|
envDir := filepath.Join(homeDir, ".config", "environment.d")
|
||||||
if err := os.MkdirAll(envDir, 0755); err != nil {
|
if err := os.MkdirAll(envDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create environment.d directory: %w", err)
|
return fmt.Errorf("failed to create environment.d directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,7 +555,7 @@ TERMINAL=%s
|
|||||||
`, terminalCmd)
|
`, terminalCmd)
|
||||||
|
|
||||||
envFile := filepath.Join(envDir, "90-dms.conf")
|
envFile := filepath.Join(envDir, "90-dms.conf")
|
||||||
if err := os.WriteFile(envFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(envFile, []byte(content), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write environment config: %w", err)
|
return fmt.Errorf("failed to write environment config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,7 +594,7 @@ func (b *BaseDistribution) WriteHyprlandSessionTarget() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
targetDir := filepath.Join(homeDir, ".config", "systemd", "user")
|
targetDir := filepath.Join(homeDir, ".config", "systemd", "user")
|
||||||
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create systemd user directory: %w", err)
|
return fmt.Errorf("failed to create systemd user directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,7 +605,7 @@ Requires=graphical-session.target
|
|||||||
After=graphical-session.target
|
After=graphical-session.target
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(targetPath, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(targetPath, []byte(content), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write hyprland-session.target: %w", err)
|
return fmt.Errorf("failed to write hyprland-session.target: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
|
|||||||
|
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
os.MkdirAll(dmsPath, 0755)
|
os.MkdirAll(dmsPath, 0o755)
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
originalHome := os.Getenv("HOME")
|
||||||
defer os.Setenv("HOME", originalHome)
|
defer os.Setenv("HOME", originalHome)
|
||||||
@@ -55,7 +55,7 @@ func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
|
|||||||
exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run()
|
exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run()
|
||||||
|
|
||||||
testFile := filepath.Join(dmsPath, "test.txt")
|
testFile := filepath.Join(dmsPath, "test.txt")
|
||||||
os.WriteFile(testFile, []byte("test"), 0644)
|
os.WriteFile(testFile, []byte("test"), 0o644)
|
||||||
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||||
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
|||||||
|
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
os.MkdirAll(dmsPath, 0755)
|
os.MkdirAll(dmsPath, 0o755)
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
originalHome := os.Getenv("HOME")
|
||||||
defer os.Setenv("HOME", originalHome)
|
defer os.Setenv("HOME", originalHome)
|
||||||
@@ -99,7 +99,7 @@ func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
|||||||
exec.Command("git", "-C", dmsPath, "remote", "add", "origin", "https://github.com/AvengeMedia/DankMaterialShell.git").Run()
|
exec.Command("git", "-C", dmsPath, "remote", "add", "origin", "https://github.com/AvengeMedia/DankMaterialShell.git").Run()
|
||||||
|
|
||||||
testFile := filepath.Join(dmsPath, "test.txt")
|
testFile := filepath.Join(dmsPath, "test.txt")
|
||||||
os.WriteFile(testFile, []byte("test"), 0644)
|
os.WriteFile(testFile, []byte("test"), 0o644)
|
||||||
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||||
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||||
exec.Command("git", "-C", dmsPath, "tag", "v0.0.1").Run()
|
exec.Command("git", "-C", dmsPath, "tag", "v0.0.1").Run()
|
||||||
@@ -125,7 +125,7 @@ func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
|||||||
func TestBaseDistribution_detectDMS_DirectoryWithoutGit(t *testing.T) {
|
func TestBaseDistribution_detectDMS_DirectoryWithoutGit(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
os.MkdirAll(dmsPath, 0755)
|
os.MkdirAll(dmsPath, 0o755)
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
originalHome := os.Getenv("HOME")
|
||||||
defer os.Setenv("HOME", originalHome)
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
|||||||
@@ -540,12 +540,12 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -576,7 +576,7 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildDir := tmpDir + "/build"
|
buildDir := tmpDir + "/build"
|
||||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create build directory: %w", err)
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
for _, dir := range parentDirs {
|
for _, dir := range parentDirs {
|
||||||
if _, err := os.Stat(dir.path); os.IsNotExist(err) {
|
if _, err := os.Stat(dir.path); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(dir.path, 0755); err != nil {
|
if err := os.MkdirAll(dir.path, 0o755); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.desc, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.desc, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -295,7 +295,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
for _, dir := range configDirs {
|
for _, dir := range configDirs {
|
||||||
if _, err := os.Stat(dir.path); os.IsNotExist(err) {
|
if _, err := os.Stat(dir.path); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(dir.path, 0755); err != nil {
|
if err := os.MkdirAll(dir.path, 0o755); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.path, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.path, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -355,14 +355,14 @@ func SyncDMSConfigs(dmsPath string, logFunc func(string), sudoPassword string) e
|
|||||||
for _, link := range symlinks {
|
for _, link := range symlinks {
|
||||||
sourceDir := filepath.Dir(link.source)
|
sourceDir := filepath.Dir(link.source)
|
||||||
if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
|
if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create directory %s: %v", sourceDir, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Could not create directory %s: %v", sourceDir, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(link.source); os.IsNotExist(err) {
|
if _, err := os.Stat(link.source); os.IsNotExist(err) {
|
||||||
if err := os.WriteFile(link.source, []byte("{}"), 0644); err != nil {
|
if err := os.WriteFile(link.source, []byte("{}"), 0o644); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", link.source, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", link.source, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -455,7 +455,7 @@ user = "greeter"
|
|||||||
newConfig := strings.Join(finalLines, "\n")
|
newConfig := strings.Join(finalLines, "\n")
|
||||||
|
|
||||||
tmpFile := "/tmp/greetd-config.toml"
|
tmpFile := "/tmp/greetd-config.toml"
|
||||||
if err := os.WriteFile(tmpFile, []byte(newConfig), 0644); err != nil {
|
if err := os.WriteFile(tmpFile, []byte(newConfig), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write temp config: %w", err)
|
return fmt.Errorf("failed to write temp config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,16 +79,16 @@ func TestFindJSONFiles(t *testing.T) {
|
|||||||
txtFile := filepath.Join(tmpDir, "readme.txt")
|
txtFile := filepath.Join(tmpDir, "readme.txt")
|
||||||
subdir := filepath.Join(tmpDir, "subdir")
|
subdir := filepath.Join(tmpDir, "subdir")
|
||||||
|
|
||||||
if err := os.WriteFile(file1, []byte("{}"), 0644); err != nil {
|
if err := os.WriteFile(file1, []byte("{}"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to create file1: %v", err)
|
t.Fatalf("Failed to create file1: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(file2, []byte("{}"), 0644); err != nil {
|
if err := os.WriteFile(file2, []byte("{}"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to create file2: %v", err)
|
t.Fatalf("Failed to create file2: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(txtFile, []byte("text"), 0644); err != nil {
|
if err := os.WriteFile(txtFile, []byte("text"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to create txt file: %v", err)
|
t.Fatalf("Failed to create txt file: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(subdir, 0755); err != nil {
|
if err := os.MkdirAll(subdir, 0o755); err != nil {
|
||||||
t.Fatalf("Failed to create subdir: %v", err)
|
t.Fatalf("Failed to create subdir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,10 +143,10 @@ func TestFindJSONFilesMultiplePaths(t *testing.T) {
|
|||||||
file1 := filepath.Join(tmpDir1, "app1.json")
|
file1 := filepath.Join(tmpDir1, "app1.json")
|
||||||
file2 := filepath.Join(tmpDir2, "app2.json")
|
file2 := filepath.Join(tmpDir2, "app2.json")
|
||||||
|
|
||||||
if err := os.WriteFile(file1, []byte("{}"), 0644); err != nil {
|
if err := os.WriteFile(file1, []byte("{}"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to create file1: %v", err)
|
t.Fatalf("Failed to create file1: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(file2, []byte("{}"), 0644); err != nil {
|
if err := os.WriteFile(file2, []byte("{}"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to create file2: %v", err)
|
t.Fatalf("Failed to create file2: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ func TestAutoDiscoverProviders(t *testing.T) {
|
|||||||
}`
|
}`
|
||||||
|
|
||||||
file := filepath.Join(tmpDir, "testapp.json")
|
file := filepath.Join(tmpDir, "testapp.json")
|
||||||
if err := os.WriteFile(file, []byte(jsonContent), 0644); err != nil {
|
if err := os.WriteFile(file, []byte(jsonContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to create test file: %v", err)
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ func TestAutoDiscoverProvidersNoFactory(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
file := filepath.Join(tmpDir, "test.json")
|
file := filepath.Join(tmpDir, "test.json")
|
||||||
if err := os.WriteFile(file, []byte("{}"), 0644); err != nil {
|
if err := os.WriteFile(file, []byte("{}"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to create test file: %v", err)
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ func (h *HyprlandProvider) SetBind(key, action, description string, options map[
|
|||||||
|
|
||||||
overridePath := h.GetOverridePath()
|
overridePath := h.GetOverridePath()
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(overridePath), 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create dms directory: %w", err)
|
return fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +398,7 @@ func (h *HyprlandProvider) getBindSortPriority(action string) int {
|
|||||||
func (h *HyprlandProvider) writeOverrideBinds(binds map[string]*hyprlandOverrideBind) error {
|
func (h *HyprlandProvider) writeOverrideBinds(binds map[string]*hyprlandOverrideBind) error {
|
||||||
overridePath := h.GetOverridePath()
|
overridePath := h.GetOverridePath()
|
||||||
content := h.generateBindsContent(binds)
|
content := h.generateBindsContent(binds)
|
||||||
return os.WriteFile(overridePath, []byte(content), 0644)
|
return os.WriteFile(overridePath, []byte(content), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) generateBindsContent(binds map[string]*hyprlandOverrideBind) string {
|
func (h *HyprlandProvider) generateBindsContent(binds map[string]*hyprlandOverrideBind) string {
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ bind = SUPER, right, movefocus, r
|
|||||||
bind = SUPER, T, exec, kitty # Terminal
|
bind = SUPER, T, exec, kitty # Terminal
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ bind = SUPER, B, exec, app2
|
|||||||
#/# = SUPER, C, exec, app3 # Custom comment
|
#/# = SUPER, C, exec, app3 # Custom comment
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,10 +278,10 @@ func TestHyprlandReadContentMultipleFiles(t *testing.T) {
|
|||||||
content1 := "bind = SUPER, Q, killactive\n"
|
content1 := "bind = SUPER, Q, killactive\n"
|
||||||
content2 := "bind = SUPER, T, exec, kitty\n"
|
content2 := "bind = SUPER, T, exec, kitty\n"
|
||||||
|
|
||||||
if err := os.WriteFile(file1, []byte(content1), 0644); err != nil {
|
if err := os.WriteFile(file1, []byte(content1), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write file1: %v", err)
|
t.Fatalf("Failed to write file1: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(file2, []byte(content2), 0644); err != nil {
|
if err := os.WriteFile(file2, []byte(content2), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write file2: %v", err)
|
t.Fatalf("Failed to write file2: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,13 +328,13 @@ func TestHyprlandReadContentWithTildeExpansion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tmpSubdir := filepath.Join(homeDir, ".config", "test-hypr-"+t.Name())
|
tmpSubdir := filepath.Join(homeDir, ".config", "test-hypr-"+t.Name())
|
||||||
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
|
if err := os.MkdirAll(tmpSubdir, 0o755); err != nil {
|
||||||
t.Skip("Cannot create test directory in home")
|
t.Skip("Cannot create test directory in home")
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpSubdir)
|
defer os.RemoveAll(tmpSubdir)
|
||||||
|
|
||||||
configFile := filepath.Join(tmpSubdir, "test.conf")
|
configFile := filepath.Join(tmpSubdir, "test.conf")
|
||||||
if err := os.WriteFile(configFile, []byte("bind = SUPER, Q, killactive\n"), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte("bind = SUPER, Q, killactive\n"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +381,7 @@ bind = SUPER, Q, killactive
|
|||||||
bind = SUPER, T, exec, kitty
|
bind = SUPER, T, exec, kitty
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ bind = SUPER, T, exec, kitty # Terminal
|
|||||||
bind = SUPER, 1, workspace, 1
|
bind = SUPER, 1, workspace, 1
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ func TestFormatKey(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) {
|
||||||
if err := os.WriteFile(configFile, []byte(tt.content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(tt.content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ func TestDescriptionFallback(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) {
|
||||||
if err := os.WriteFile(configFile, []byte(tt.content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(tt.content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ func (m *MangoWCProvider) SetBind(key, action, description string, options map[s
|
|||||||
|
|
||||||
overridePath := m.GetOverridePath()
|
overridePath := m.GetOverridePath()
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(overridePath), 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create dms directory: %w", err)
|
return fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +360,7 @@ func (m *MangoWCProvider) getBindSortPriority(action string) int {
|
|||||||
func (m *MangoWCProvider) writeOverrideBinds(binds map[string]*mangowcOverrideBind) error {
|
func (m *MangoWCProvider) writeOverrideBinds(binds map[string]*mangowcOverrideBind) error {
|
||||||
overridePath := m.GetOverridePath()
|
overridePath := m.GetOverridePath()
|
||||||
content := m.generateBindsContent(binds)
|
content := m.generateBindsContent(binds)
|
||||||
return os.WriteFile(overridePath, []byte(content), 0644)
|
return os.WriteFile(overridePath, []byte(content), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverrideBind) string {
|
func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverrideBind) string {
|
||||||
|
|||||||
@@ -502,17 +502,17 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
|
|||||||
p.dmsProcessed = true
|
p.dmsProcessed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fullPath := sourcePath
|
expanded, err := utils.ExpandPath(sourcePath)
|
||||||
if !filepath.IsAbs(sourcePath) {
|
|
||||||
fullPath = filepath.Join(baseDir, sourcePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
expanded, err := utils.ExpandPath(fullPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
includedBinds, err := p.parseFileWithSource(expanded)
|
fullPath := expanded
|
||||||
|
if !filepath.IsAbs(expanded) {
|
||||||
|
fullPath = filepath.Join(baseDir, expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
includedBinds, err := p.parseFileWithSource(fullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -521,33 +521,10 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyBinding {
|
func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyBinding {
|
||||||
data, err := os.ReadFile(dmsBindsPath)
|
keybinds, err := p.parseFileWithSource(dmsBindsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 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
|
p.dmsProcessed = true
|
||||||
return keybinds
|
return keybinds
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ bind=Ctrl,1,view,1,0
|
|||||||
bind=Ctrl,2,view,2,0
|
bind=Ctrl,2,view,2,0
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,10 +276,10 @@ func TestMangoWCReadContentMultipleFiles(t *testing.T) {
|
|||||||
content1 := "bind=ALT,q,killclient,\n"
|
content1 := "bind=ALT,q,killclient,\n"
|
||||||
content2 := "bind=Alt,t,spawn,kitty\n"
|
content2 := "bind=Alt,t,spawn,kitty\n"
|
||||||
|
|
||||||
if err := os.WriteFile(file1, []byte(content1), 0644); err != nil {
|
if err := os.WriteFile(file1, []byte(content1), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write file1: %v", err)
|
t.Fatalf("Failed to write file1: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(file2, []byte(content2), 0644); err != nil {
|
if err := os.WriteFile(file2, []byte(content2), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write file2: %v", err)
|
t.Fatalf("Failed to write file2: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +300,7 @@ func TestMangoWCReadContentSingleFile(t *testing.T) {
|
|||||||
|
|
||||||
content := "bind=ALT,q,killclient,\n"
|
content := "bind=ALT,q,killclient,\n"
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write config: %v", err)
|
t.Fatalf("Failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,13 +347,13 @@ func TestMangoWCReadContentWithTildeExpansion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tmpSubdir := filepath.Join(homeDir, ".config", "test-mango-"+t.Name())
|
tmpSubdir := filepath.Join(homeDir, ".config", "test-mango-"+t.Name())
|
||||||
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
|
if err := os.MkdirAll(tmpSubdir, 0o755); err != nil {
|
||||||
t.Skip("Cannot create test directory in home")
|
t.Skip("Cannot create test directory in home")
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpSubdir)
|
defer os.RemoveAll(tmpSubdir)
|
||||||
|
|
||||||
configFile := filepath.Join(tmpSubdir, "config.conf")
|
configFile := filepath.Join(tmpSubdir, "config.conf")
|
||||||
if err := os.WriteFile(configFile, []byte("bind=ALT,q,killclient,\n"), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte("bind=ALT,q,killclient,\n"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ bind=ALT,q,killclient,
|
|||||||
bind=Alt,t,spawn,kitty
|
bind=Alt,t,spawn,kitty
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,7 +458,7 @@ bind=Ctrl,2,view,2,0
|
|||||||
bind=Ctrl,3,view,3,0
|
bind=Ctrl,3,view,3,0
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ bind=SUPER,n,switch_layout
|
|||||||
bind=ALT+SHIFT,X,incgaps,1
|
bind=ALT+SHIFT,X,incgaps,1
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ bind=ALT,Left,focusdir,left
|
|||||||
bind=Ctrl,1,view,1,0
|
bind=Ctrl,1,view,1,0
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ func (n *NiriProvider) SetBind(key, action, description string, options map[stri
|
|||||||
|
|
||||||
overridePath := n.GetOverridePath()
|
overridePath := n.GetOverridePath()
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(overridePath), 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create dms directory: %w", err)
|
return fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,7 +485,7 @@ func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(overridePath, []byte(content), 0644)
|
return os.WriteFile(overridePath, []byte(content), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) getBindSortPriority(action string) int {
|
func (n *NiriProvider) getBindSortPriority(action string) int {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func TestNiriParseBasicBinds(t *testing.T) {
|
|||||||
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ func TestNiriParseRecentWindows(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ func TestNiriParseRecentWindows(t *testing.T) {
|
|||||||
func TestNiriParseInclude(t *testing.T) {
|
func TestNiriParseInclude(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
subDir := filepath.Join(tmpDir, "dms")
|
subDir := filepath.Join(tmpDir, "dms")
|
||||||
if err := os.MkdirAll(subDir, 0755); err != nil {
|
if err := os.MkdirAll(subDir, 0o755); err != nil {
|
||||||
t.Fatalf("Failed to create subdir: %v", err)
|
t.Fatalf("Failed to create subdir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,10 +165,10 @@ include "dms/binds.kdl"
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write main config: %v", err)
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
|
if err := os.WriteFile(includeConfig, []byte(includeContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write include config: %v", err)
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ include "dms/binds.kdl"
|
|||||||
func TestNiriParseIncludeOverride(t *testing.T) {
|
func TestNiriParseIncludeOverride(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
subDir := filepath.Join(tmpDir, "dms")
|
subDir := filepath.Join(tmpDir, "dms")
|
||||||
if err := os.MkdirAll(subDir, 0755); err != nil {
|
if err := os.MkdirAll(subDir, 0o755); err != nil {
|
||||||
t.Fatalf("Failed to create subdir: %v", err)
|
t.Fatalf("Failed to create subdir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,10 +202,10 @@ include "dms/binds.kdl"
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write main config: %v", err)
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
|
if err := os.WriteFile(includeConfig, []byte(includeContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write include config: %v", err)
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,10 +246,10 @@ include "other.kdl"
|
|||||||
include "config.kdl"
|
include "config.kdl"
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write main config: %v", err)
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(otherConfig, []byte(otherContent), 0644); err != nil {
|
if err := os.WriteFile(otherConfig, []byte(otherContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write other config: %v", err)
|
t.Fatalf("Failed to write other config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ func TestNiriParseMissingInclude(t *testing.T) {
|
|||||||
}
|
}
|
||||||
include "nonexistent/file.kdl"
|
include "nonexistent/file.kdl"
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +301,7 @@ input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ func TestNiriBindOverrideBehavior(t *testing.T) {
|
|||||||
Mod+T hotkey-overlay-title="Third" { spawn "third"; }
|
Mod+T hotkey-overlay-title="Third" { spawn "third"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,7 +386,7 @@ func TestNiriBindOverrideBehavior(t *testing.T) {
|
|||||||
func TestNiriBindOverrideWithIncludes(t *testing.T) {
|
func TestNiriBindOverrideWithIncludes(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
subDir := filepath.Join(tmpDir, "custom")
|
subDir := filepath.Join(tmpDir, "custom")
|
||||||
if err := os.MkdirAll(subDir, 0755); err != nil {
|
if err := os.MkdirAll(subDir, 0o755); err != nil {
|
||||||
t.Fatalf("Failed to create subdir: %v", err)
|
t.Fatalf("Failed to create subdir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,10 +409,10 @@ binds {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write main config: %v", err)
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
|
if err := os.WriteFile(includeConfig, []byte(includeContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write include config: %v", err)
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,7 +471,7 @@ func TestNiriParseMultipleArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,7 +508,7 @@ func TestNiriParseNumericWorkspaceBinds(t *testing.T) {
|
|||||||
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1; }
|
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,7 +550,7 @@ func TestNiriParseQuotedStringArgs(t *testing.T) {
|
|||||||
Super+Shift+Minus hotkey-overlay-title="Adjust Window Height -10%" { set-window-height "-10%"; }
|
Super+Shift+Minus hotkey-overlay-title="Adjust Window Height -10%" { set-window-height "-10%"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,7 +586,7 @@ func TestNiriParseActionWithProperties(t *testing.T) {
|
|||||||
Alt+Tab { next-window scope="output"; }
|
Alt+Tab { next-window scope="output"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func TestNiriProviderGetCheatSheet(t *testing.T) {
|
|||||||
Mod+Shift+E { quit; }
|
Mod+Shift+E { quit; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +312,7 @@ func TestNiriGenerateBindsContentRoundTrip(t *testing.T) {
|
|||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configFile := filepath.Join(tmpDir, "config.kdl")
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write temp file: %v", err)
|
t.Fatalf("Failed to write temp file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,12 +351,12 @@ func TestNiriEmptyArgsPreservation(t *testing.T) {
|
|||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
dmsDir := filepath.Join(tmpDir, "dms")
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
if err := os.MkdirAll(dmsDir, 0755); err != nil {
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
t.Fatalf("Failed to create dms directory: %v", err)
|
t.Fatalf("Failed to create dms directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
bindsFile := filepath.Join(dmsDir, "binds.kdl")
|
bindsFile := filepath.Join(dmsDir, "binds.kdl")
|
||||||
if err := os.WriteFile(bindsFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(bindsFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write binds file: %v", err)
|
t.Fatalf("Failed to write binds file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,7 +428,7 @@ recent-windows {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,7 +621,7 @@ func TestNiriGenerateWorkspaceBindsRoundTrip(t *testing.T) {
|
|||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configFile := filepath.Join(tmpDir, "config.kdl")
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write temp file: %v", err)
|
t.Fatalf("Failed to write temp file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ bindsym $mod+t exec $term
|
|||||||
bindsym $mod+d exec $menu
|
bindsym $mod+d exec $menu
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ bindsym $mod+Right focus right
|
|||||||
bindsym $mod+t exec kitty # Terminal
|
bindsym $mod+t exec kitty # Terminal
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,13 +328,13 @@ func TestSwayReadContentWithTildeExpansion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tmpSubdir := filepath.Join(homeDir, ".config", "test-sway-"+t.Name())
|
tmpSubdir := filepath.Join(homeDir, ".config", "test-sway-"+t.Name())
|
||||||
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
|
if err := os.MkdirAll(tmpSubdir, 0o755); err != nil {
|
||||||
t.Skip("Cannot create test directory in home")
|
t.Skip("Cannot create test directory in home")
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpSubdir)
|
defer os.RemoveAll(tmpSubdir)
|
||||||
|
|
||||||
configFile := filepath.Join(tmpSubdir, "config")
|
configFile := filepath.Join(tmpSubdir, "config")
|
||||||
if err := os.WriteFile(configFile, []byte("bindsym Mod4+q kill\n"), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte("bindsym Mod4+q kill\n"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +365,7 @@ bindsym Mod4+q kill
|
|||||||
bindsym Mod4+t exec kitty
|
bindsym Mod4+t exec kitty
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +404,7 @@ bindsym $mod+2 workspace number 2
|
|||||||
bindsym $mod+Shift+1 move container to workspace number 1
|
bindsym $mod+Shift+1 move container to workspace number 1
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ bindsym $mod+s layout stacking
|
|||||||
bindsym $mod+w layout tabbed
|
bindsym $mod+w layout tabbed
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ bindsym $mod+f fullscreen toggle
|
|||||||
bindsym $mod+1 workspace number 1
|
bindsym $mod+1 workspace number 1
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ func Run(opts Options) error {
|
|||||||
opts.AppChecker = utils.DefaultAppChecker{}
|
opts.AppChecker = utils.DefaultAppChecker{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(opts.StateDir, 0755); err != nil {
|
if err := os.MkdirAll(opts.StateDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create state dir: %w", err)
|
return fmt.Errorf("failed to create state dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +414,7 @@ func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir string, checkC
|
|||||||
|
|
||||||
modified := strings.ReplaceAll(string(origData), ".default.", ".dark.")
|
modified := strings.ReplaceAll(string(origData), ".default.", ".dark.")
|
||||||
tmpPath := filepath.Join(tmpDir, templateName)
|
tmpPath := filepath.Join(tmpDir, templateName)
|
||||||
if err := os.WriteFile(tmpPath, []byte(modified), 0644); err != nil {
|
if err := os.WriteFile(tmpPath, []byte(modified), 0o644); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ func TestAppendConfigBinaryExists(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testConfig := "test config content"
|
testConfig := "test config content"
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
||||||
t.Fatalf("failed to write config: %v", err)
|
t.Fatalf("failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,13 +58,13 @@ func TestAppendConfigBinaryDoesNotExist(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testConfig := "test config content"
|
testConfig := "test config content"
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
||||||
t.Fatalf("failed to write config: %v", err)
|
t.Fatalf("failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,13 +99,13 @@ func TestAppendConfigFlatpakExists(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testConfig := "zen config content"
|
testConfig := "zen config content"
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
||||||
t.Fatalf("failed to write config: %v", err)
|
t.Fatalf("failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,13 +139,13 @@ func TestAppendConfigFlatpakDoesNotExist(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testConfig := "test config content"
|
testConfig := "test config content"
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
||||||
t.Fatalf("failed to write config: %v", err)
|
t.Fatalf("failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,13 +180,13 @@ func TestAppendConfigBothExist(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testConfig := "zen config content"
|
testConfig := "zen config content"
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
||||||
t.Fatalf("failed to write config: %v", err)
|
t.Fatalf("failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,13 +220,13 @@ func TestAppendConfigNeitherExists(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testConfig := "test config content"
|
testConfig := "test config content"
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
||||||
t.Fatalf("failed to write config: %v", err)
|
t.Fatalf("failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,13 +261,13 @@ func TestAppendConfigNoChecks(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testConfig := "always include"
|
testConfig := "always include"
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
||||||
t.Fatalf("failed to write config: %v", err)
|
t.Fatalf("failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +298,7 @@ func TestAppendConfigFileDoesNotExist(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,12 +116,12 @@ func (m *Manager) Install(plugin Plugin) error {
|
|||||||
return fmt.Errorf("plugin already installed: %s", plugin.Name)
|
return fmt.Errorf("plugin already installed: %s", plugin.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.fs.MkdirAll(m.pluginsDir, 0755); err != nil {
|
if err := m.fs.MkdirAll(m.pluginsDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create plugins directory: %w", err)
|
return fmt.Errorf("failed to create plugins directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
||||||
if err := m.fs.MkdirAll(reposDir, 0755); err != nil {
|
if err := m.fs.MkdirAll(reposDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create repos directory: %w", err)
|
return fmt.Errorf("failed to create repos directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ func (m *Manager) Install(plugin Plugin) error {
|
|||||||
|
|
||||||
metaPath := pluginPath + ".meta"
|
metaPath := pluginPath + ".meta"
|
||||||
metaContent := fmt.Sprintf("repo=%s\npath=%s\nrepodir=%s", plugin.Repo, plugin.Path, repoName)
|
metaContent := fmt.Sprintf("repo=%s\npath=%s\nrepodir=%s", plugin.Repo, plugin.Path, repoName)
|
||||||
if err := afero.WriteFile(m.fs, metaPath, []byte(metaContent), 0644); err != nil {
|
if err := afero.WriteFile(m.fs, metaPath, []byte(metaContent), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write metadata: %w", err)
|
return fmt.Errorf("failed to write metadata: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func TestIsInstalled(t *testing.T) {
|
|||||||
|
|
||||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||||
err := fs.MkdirAll(pluginPath, 0755)
|
err := fs.MkdirAll(pluginPath, 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
installed, err := manager.IsInstalled(plugin)
|
installed, err := manager.IsInstalled(plugin)
|
||||||
@@ -100,7 +100,7 @@ func TestInstall(t *testing.T) {
|
|||||||
cloneCalled = true
|
cloneCalled = true
|
||||||
assert.Equal(t, filepath.Join(pluginsDir, plugin.ID), path)
|
assert.Equal(t, filepath.Join(pluginsDir, plugin.ID), path)
|
||||||
assert.Equal(t, plugin.Repo, url)
|
assert.Equal(t, plugin.Repo, url)
|
||||||
return fs.MkdirAll(path, 0755)
|
return fs.MkdirAll(path, 0o755)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
manager.gitClient = mockGit
|
manager.gitClient = mockGit
|
||||||
@@ -118,7 +118,7 @@ func TestInstall(t *testing.T) {
|
|||||||
|
|
||||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||||
err := fs.MkdirAll(pluginPath, 0755)
|
err := fs.MkdirAll(pluginPath, 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = manager.Install(plugin)
|
err = manager.Install(plugin)
|
||||||
@@ -137,7 +137,7 @@ func TestManagerUpdate(t *testing.T) {
|
|||||||
|
|
||||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||||
err := fs.MkdirAll(pluginPath, 0755)
|
err := fs.MkdirAll(pluginPath, 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
pullCalled := false
|
pullCalled := false
|
||||||
@@ -171,7 +171,7 @@ func TestUninstall(t *testing.T) {
|
|||||||
|
|
||||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||||
err := fs.MkdirAll(pluginPath, 0755)
|
err := fs.MkdirAll(pluginPath, 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = manager.Uninstall(plugin)
|
err = manager.Uninstall(plugin)
|
||||||
@@ -195,14 +195,14 @@ func TestListInstalled(t *testing.T) {
|
|||||||
t.Run("lists installed plugins", func(t *testing.T) {
|
t.Run("lists installed plugins", func(t *testing.T) {
|
||||||
manager, fs, pluginsDir := setupTestManager(t)
|
manager, fs, pluginsDir := setupTestManager(t)
|
||||||
|
|
||||||
err := fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0755)
|
err := fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0644)
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin2"), 0755)
|
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin2"), 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin2", "plugin.json"), []byte(`{"id":"Plugin2"}`), 0644)
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin2", "plugin.json"), []byte(`{"id":"Plugin2"}`), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
installed, err := manager.ListInstalled()
|
installed, err := manager.ListInstalled()
|
||||||
@@ -223,15 +223,15 @@ func TestListInstalled(t *testing.T) {
|
|||||||
t.Run("ignores files and .repos directory", func(t *testing.T) {
|
t.Run("ignores files and .repos directory", func(t *testing.T) {
|
||||||
manager, fs, pluginsDir := setupTestManager(t)
|
manager, fs, pluginsDir := setupTestManager(t)
|
||||||
|
|
||||||
err := fs.MkdirAll(pluginsDir, 0755)
|
err := fs.MkdirAll(pluginsDir, 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0755)
|
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0644)
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = fs.MkdirAll(filepath.Join(pluginsDir, ".repos"), 0755)
|
err = fs.MkdirAll(filepath.Join(pluginsDir, ".repos"), 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("test"), 0644)
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("test"), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
installed, err := manager.ListInstalled()
|
installed, err := manager.ListInstalled()
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ func (r *Registry) Update() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
|
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ func (r *Registry) Update() error {
|
|||||||
return fmt.Errorf("failed to remove corrupted registry: %w", err)
|
return fmt.Errorf("failed to remove corrupted registry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
|
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,13 +63,13 @@ func setupTestRegistry(t *testing.T) (*Registry, afero.Fs, string) {
|
|||||||
|
|
||||||
func createTestPlugin(t *testing.T, fs afero.Fs, dir string, filename string, plugin Plugin) {
|
func createTestPlugin(t *testing.T, fs afero.Fs, dir string, filename string, plugin Plugin) {
|
||||||
pluginsDir := filepath.Join(dir, "plugins")
|
pluginsDir := filepath.Join(dir, "plugins")
|
||||||
err := fs.MkdirAll(pluginsDir, 0755)
|
err := fs.MkdirAll(pluginsDir, 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
data, err := json.Marshal(plugin)
|
data, err := json.Marshal(plugin)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, filename), data, 0644)
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, filename), data, 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,10 +118,10 @@ func TestLoadPlugins(t *testing.T) {
|
|||||||
registry, fs, tmpDir := setupTestRegistry(t)
|
registry, fs, tmpDir := setupTestRegistry(t)
|
||||||
|
|
||||||
pluginsDir := filepath.Join(tmpDir, "plugins")
|
pluginsDir := filepath.Join(tmpDir, "plugins")
|
||||||
err := fs.MkdirAll(pluginsDir, 0755)
|
err := fs.MkdirAll(pluginsDir, 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("# Test"), 0644)
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("# Test"), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
@@ -146,7 +146,7 @@ func TestLoadPlugins(t *testing.T) {
|
|||||||
registry, fs, tmpDir := setupTestRegistry(t)
|
registry, fs, tmpDir := setupTestRegistry(t)
|
||||||
|
|
||||||
pluginsDir := filepath.Join(tmpDir, "plugins")
|
pluginsDir := filepath.Join(tmpDir, "plugins")
|
||||||
err := fs.MkdirAll(filepath.Join(pluginsDir, "subdir"), 0755)
|
err := fs.MkdirAll(filepath.Join(pluginsDir, "subdir"), 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
@@ -170,10 +170,10 @@ func TestLoadPlugins(t *testing.T) {
|
|||||||
registry, fs, tmpDir := setupTestRegistry(t)
|
registry, fs, tmpDir := setupTestRegistry(t)
|
||||||
|
|
||||||
pluginsDir := filepath.Join(tmpDir, "plugins")
|
pluginsDir := filepath.Join(tmpDir, "plugins")
|
||||||
err := fs.MkdirAll(pluginsDir, 0755)
|
err := fs.MkdirAll(pluginsDir, 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "invalid.json"), []byte("{invalid json}"), 0644)
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "invalid.json"), []byte("{invalid json}"), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
@@ -303,7 +303,7 @@ func TestUpdate(t *testing.T) {
|
|||||||
Distro: []string{"any"},
|
Distro: []string{"any"},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := fs.MkdirAll(tmpDir, 0755)
|
err := fs.MkdirAll(tmpDir, 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
pullCalled := false
|
pullCalled := false
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ func GetOutputDir() string {
|
|||||||
|
|
||||||
if xdgPics := getXDGPicturesDir(); xdgPics != "" {
|
if xdgPics := getXDGPicturesDir(); xdgPics != "" {
|
||||||
screenshotDir := filepath.Join(xdgPics, "Screenshots")
|
screenshotDir := filepath.Join(xdgPics, "Screenshots")
|
||||||
if err := os.MkdirAll(screenshotDir, 0755); err == nil {
|
if err := os.MkdirAll(screenshotDir, 0o755); err == nil {
|
||||||
return screenshotDir
|
return screenshotDir
|
||||||
}
|
}
|
||||||
return xdgPics
|
return xdgPics
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func LoadState() (*PersistentState, error) {
|
|||||||
func SaveState(state *PersistentState) error {
|
func SaveState(state *PersistentState) error {
|
||||||
path := getStateFilePath()
|
path := getStateFilePath()
|
||||||
dir := filepath.Dir(path)
|
dir := filepath.Dir(path)
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ func SaveState(state *PersistentState) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.WriteFile(path, data, 0644)
|
return os.WriteFile(path, data, 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLastRegion() Region {
|
func GetLastRegion() Region {
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ func (b *SysfsBackend) SetBrightnessWithExponent(id string, percent int, exponen
|
|||||||
brightnessPath := filepath.Join(devicePath, "brightness")
|
brightnessPath := filepath.Join(devicePath, "brightness")
|
||||||
|
|
||||||
data := []byte(fmt.Sprintf("%d", value))
|
data := []byte(fmt.Sprintf("%d", value))
|
||||||
if err := os.WriteFile(brightnessPath, data, 0644); err != nil {
|
if err := os.WriteFile(brightnessPath, data, 0o644); err != nil {
|
||||||
return fmt.Errorf("write brightness: %w", err)
|
return fmt.Errorf("write brightness: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
||||||
if err := os.MkdirAll(backlightDir, 0755); err != nil {
|
if err := os.MkdirAll(backlightDir, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,13 +86,13 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
||||||
if err := os.MkdirAll(backlightDir, 0755); err != nil {
|
if err := os.MkdirAll(backlightDir, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,13 +158,13 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
||||||
if err := os.MkdirAll(backlightDir, 0755); err != nil {
|
if err := os.MkdirAll(backlightDir, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,13 +215,13 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
ledsDir := filepath.Join(tmpDir, "leds", "test_led")
|
ledsDir := filepath.Join(tmpDir, "leds", "test_led")
|
||||||
if err := os.MkdirAll(ledsDir, 0755); err != nil {
|
if err := os.MkdirAll(ledsDir, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(ledsDir, "max_brightness"), []byte("255\n"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(ledsDir, "max_brightness"), []byte("255\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(ledsDir, "brightness"), []byte("128\n"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(ledsDir, "brightness"), []byte("128\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,26 +136,26 @@ func TestSysfsBackend_ScanDevices(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
||||||
if err := os.MkdirAll(backlightDir, 0755); err != nil {
|
if err := os.MkdirAll(backlightDir, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ledsDir := filepath.Join(tmpDir, "leds", "test_led")
|
ledsDir := filepath.Join(tmpDir, "leds", "test_led")
|
||||||
if err := os.MkdirAll(ledsDir, 0755); err != nil {
|
if err := os.MkdirAll(ledsDir, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(filepath.Join(ledsDir, "max_brightness"), []byte("255\n"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(ledsDir, "max_brightness"), []byte("255\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(ledsDir, "brightness"), []byte("128\n"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(ledsDir, "brightness"), []byte("128\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ func setupTestManager(t *testing.T) (*Manager, string) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
backlightDir := filepath.Join(tmpDir, "backlight", "intel_backlight")
|
backlightDir := filepath.Join(tmpDir, "backlight", "intel_backlight")
|
||||||
if err := os.MkdirAll(backlightDir, 0755); err != nil {
|
if err := os.MkdirAll(backlightDir, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("1000\n"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("1000\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("500\n"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("500\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ func TestHandleEvent_ChangeAction(t *testing.T) {
|
|||||||
um := &UdevMonitor{stop: make(chan struct{})}
|
um := &UdevMonitor{stop: make(chan struct{})}
|
||||||
|
|
||||||
brightnessPath := filepath.Join(tmpDir, "backlight", "intel_backlight", "brightness")
|
brightnessPath := filepath.Join(tmpDir, "backlight", "intel_backlight", "brightness")
|
||||||
if err := os.WriteFile(brightnessPath, []byte("800\n"), 0644); err != nil {
|
if err := os.WriteFile(brightnessPath, []byte("800\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ func TestHandleChange_InvalidBrightnessValue(t *testing.T) {
|
|||||||
um := &UdevMonitor{stop: make(chan struct{})}
|
um := &UdevMonitor{stop: make(chan struct{})}
|
||||||
|
|
||||||
brightnessPath := filepath.Join(tmpDir, "backlight", "intel_backlight", "brightness")
|
brightnessPath := filepath.Join(tmpDir, "backlight", "intel_backlight", "brightness")
|
||||||
if err := os.WriteFile(brightnessPath, []byte("not_a_number\n"), 0644); err != nil {
|
if err := os.WriteFile(brightnessPath, []byte("not_a_number\n"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
|
|||||||
handleGetPinnedEntries(conn, req, m)
|
handleGetPinnedEntries(conn, req, m)
|
||||||
case "clipboard.getPinnedCount":
|
case "clipboard.getPinnedCount":
|
||||||
handleGetPinnedCount(conn, req, m)
|
handleGetPinnedCount(conn, req, m)
|
||||||
|
case "clipboard.copyFile":
|
||||||
|
handleCopyFile(conn, req, m)
|
||||||
default:
|
default:
|
||||||
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
|
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
|
||||||
}
|
}
|
||||||
@@ -126,11 +128,29 @@ func handleCopyEntry(conn net.Conn, req models.Request, m *Manager) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filePath := m.EntryToFile(entry)
|
||||||
|
if filePath != "" {
|
||||||
|
if err := m.CopyFile(filePath); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
models.Respond(conn, req.ID, map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"filePath": filePath,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := m.SetClipboard(entry.Data, entry.MimeType); err != nil {
|
if err := m.SetClipboard(entry.Data, entry.MimeType); err != nil {
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := m.TouchEntry(uint64(id)); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"})
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,3 +301,18 @@ func handleGetPinnedCount(conn net.Conn, req models.Request, m *Manager) {
|
|||||||
count := m.GetPinnedCount()
|
count := m.GetPinnedCount()
|
||||||
models.Respond(conn, req.ID, map[string]int{"count": count})
|
models.Respond(conn, req.ID, map[string]int{"count": count})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleCopyFile(conn net.Conn, req models.Request, m *Manager) {
|
||||||
|
filePath, err := params.String(req.Params, "filePath")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.CopyFile(filePath); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied"})
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
_ "image/png"
|
_ "image/png"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -19,8 +20,10 @@ import (
|
|||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
_ "golang.org/x/image/bmp"
|
_ "golang.org/x/image/bmp"
|
||||||
_ "golang.org/x/image/tiff"
|
_ "golang.org/x/image/tiff"
|
||||||
|
_ "golang.org/x/image/webp"
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
|
|
||||||
@@ -104,7 +107,7 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
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, 0o644, &bolt.Options{
|
||||||
Timeout: 1 * time.Second,
|
Timeout: 1 * time.Second,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -316,6 +319,13 @@ func (m *Manager) readAndStore(r *os.File, mimeType string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) storeClipboardEntry(data []byte, mimeType string) {
|
func (m *Manager) storeClipboardEntry(data []byte, mimeType string) {
|
||||||
|
if mimeType == "text/uri-list" {
|
||||||
|
if imgData, imgMime, ok := m.tryReadImageFromURI(data); ok {
|
||||||
|
data = imgData
|
||||||
|
mimeType = imgMime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
entry := Entry{
|
entry := Entry{
|
||||||
Data: data,
|
Data: data,
|
||||||
MimeType: mimeType,
|
MimeType: mimeType,
|
||||||
@@ -327,6 +337,8 @@ func (m *Manager) storeClipboardEntry(data []byte, mimeType string) {
|
|||||||
switch {
|
switch {
|
||||||
case entry.IsImage:
|
case entry.IsImage:
|
||||||
entry.Preview = m.imagePreview(data, mimeType)
|
entry.Preview = m.imagePreview(data, mimeType)
|
||||||
|
case mimeType == "text/uri-list":
|
||||||
|
entry.Preview, entry.IsImage = m.uriListPreview(data)
|
||||||
default:
|
default:
|
||||||
entry.Preview = m.textPreview(data)
|
entry.Preview = m.textPreview(data)
|
||||||
}
|
}
|
||||||
@@ -493,10 +505,10 @@ func computeHash(data []byte) uint64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func extractHash(data []byte) uint64 {
|
func extractHash(data []byte) uint64 {
|
||||||
if len(data) < 8 {
|
if len(data) < 9 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return binary.BigEndian.Uint64(data[len(data)-8:])
|
return binary.BigEndian.Uint64(data[len(data)-9 : len(data)-1])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) hasSensitiveMimeType(mimes []string) bool {
|
func (m *Manager) hasSensitiveMimeType(mimes []string) bool {
|
||||||
@@ -507,6 +519,7 @@ func (m *Manager) hasSensitiveMimeType(mimes []string) bool {
|
|||||||
|
|
||||||
func (m *Manager) selectMimeType(mimes []string) string {
|
func (m *Manager) selectMimeType(mimes []string) string {
|
||||||
preferredTypes := []string{
|
preferredTypes := []string{
|
||||||
|
"text/uri-list",
|
||||||
"text/plain;charset=utf-8",
|
"text/plain;charset=utf-8",
|
||||||
"text/plain",
|
"text/plain",
|
||||||
"UTF8_STRING",
|
"UTF8_STRING",
|
||||||
@@ -527,8 +540,14 @@ func (m *Manager) selectMimeType(mimes []string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(mimes) > 0 {
|
// Skip useless MIME types when falling back
|
||||||
return mimes[0]
|
for _, mime := range mimes {
|
||||||
|
switch mime {
|
||||||
|
case "application/vnd.portal.filetransfer":
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
return mime
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
@@ -557,6 +576,62 @@ func (m *Manager) imagePreview(data []byte, format string) string {
|
|||||||
return fmt.Sprintf("[[ image %s %s %dx%d ]]", sizeStr(len(data)), imgFmt, config.Width, config.Height)
|
return fmt.Sprintf("[[ image %s %s %dx%d ]]", sizeStr(len(data)), imgFmt, config.Width, config.Height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) uriListPreview(data []byte) (string, bool) {
|
||||||
|
text := strings.TrimSpace(string(data))
|
||||||
|
uris := strings.Split(text, "\r\n")
|
||||||
|
if len(uris) == 0 {
|
||||||
|
uris = strings.Split(text, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(uris) == 1 && strings.HasPrefix(uris[0], "file://") {
|
||||||
|
filePath := strings.TrimPrefix(uris[0], "file://")
|
||||||
|
if info, err := os.Stat(filePath); err == nil && !info.IsDir() {
|
||||||
|
if imgData, err := os.ReadFile(filePath); err == nil {
|
||||||
|
if config, imgFmt, err := image.DecodeConfig(bytes.NewReader(imgData)); err == nil {
|
||||||
|
return fmt.Sprintf("[[ file %s %s %dx%d ]]", filepath.Base(filePath), imgFmt, config.Width, config.Height), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)), false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(uris) > 1 {
|
||||||
|
return fmt.Sprintf("[[ %d files ]]", len(uris)), false
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.textPreview(data), false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) tryReadImageFromURI(data []byte) ([]byte, string, bool) {
|
||||||
|
text := strings.TrimSpace(string(data))
|
||||||
|
uris := strings.Split(text, "\r\n")
|
||||||
|
if len(uris) == 0 {
|
||||||
|
uris = strings.Split(text, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(uris) != 1 || !strings.HasPrefix(uris[0], "file://") {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := strings.TrimPrefix(uris[0], "file://")
|
||||||
|
info, err := os.Stat(filePath)
|
||||||
|
if err != nil || info.IsDir() {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
imgData, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
_, imgFmt, err := image.DecodeConfig(bytes.NewReader(imgData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return imgData, "image/" + imgFmt, true
|
||||||
|
}
|
||||||
|
|
||||||
func sizeStr(size int) string {
|
func sizeStr(size int) string {
|
||||||
units := []string{"B", "KiB", "MiB"}
|
units := []string{"B", "KiB", "MiB"}
|
||||||
var i int
|
var i int
|
||||||
@@ -745,6 +820,28 @@ func (m *Manager) DeleteEntry(id uint64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) TouchEntry(id uint64) error {
|
||||||
|
if m.db == nil {
|
||||||
|
return fmt.Errorf("database not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := m.GetEntry(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.Timestamp = time.Now()
|
||||||
|
|
||||||
|
if err := m.storeEntry(*entry); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.updateState()
|
||||||
|
m.notifySubscribers()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) ClearHistory() {
|
func (m *Manager) ClearHistory() {
|
||||||
if m.db == nil {
|
if m.db == nil {
|
||||||
return
|
return
|
||||||
@@ -810,23 +907,23 @@ func (m *Manager) compactDB() error {
|
|||||||
tmpPath := m.dbPath + ".compact"
|
tmpPath := m.dbPath + ".compact"
|
||||||
defer os.Remove(tmpPath)
|
defer os.Remove(tmpPath)
|
||||||
|
|
||||||
srcDB, err := bolt.Open(m.dbPath, 0644, &bolt.Options{ReadOnly: true, Timeout: time.Second})
|
srcDB, err := bolt.Open(m.dbPath, 0o644, &bolt.Options{ReadOnly: true, Timeout: time.Second})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.db, _ = bolt.Open(m.dbPath, 0644, &bolt.Options{Timeout: time.Second})
|
m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||||
return fmt.Errorf("open source: %w", err)
|
return fmt.Errorf("open source: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dstDB, err := bolt.Open(tmpPath, 0644, &bolt.Options{Timeout: time.Second})
|
dstDB, err := bolt.Open(tmpPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
srcDB.Close()
|
srcDB.Close()
|
||||||
m.db, _ = bolt.Open(m.dbPath, 0644, &bolt.Options{Timeout: time.Second})
|
m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||||
return fmt.Errorf("open destination: %w", err)
|
return fmt.Errorf("open destination: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bolt.Compact(dstDB, srcDB, 0); err != nil {
|
if err := bolt.Compact(dstDB, srcDB, 0); err != nil {
|
||||||
srcDB.Close()
|
srcDB.Close()
|
||||||
dstDB.Close()
|
dstDB.Close()
|
||||||
m.db, _ = bolt.Open(m.dbPath, 0644, &bolt.Options{Timeout: time.Second})
|
m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||||
return fmt.Errorf("compact: %w", err)
|
return fmt.Errorf("compact: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -834,11 +931,11 @@ func (m *Manager) compactDB() error {
|
|||||||
dstDB.Close()
|
dstDB.Close()
|
||||||
|
|
||||||
if err := os.Rename(tmpPath, m.dbPath); err != nil {
|
if err := os.Rename(tmpPath, m.dbPath); err != nil {
|
||||||
m.db, _ = bolt.Open(m.dbPath, 0644, &bolt.Options{Timeout: time.Second})
|
m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||||
return fmt.Errorf("rename: %w", err)
|
return fmt.Errorf("rename: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.db, err = bolt.Open(m.dbPath, 0644, &bolt.Options{Timeout: time.Second})
|
m.db, err = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("reopen: %w", err)
|
return fmt.Errorf("reopen: %w", err)
|
||||||
}
|
}
|
||||||
@@ -885,11 +982,21 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
||||||
|
m.ownerLock.Lock()
|
||||||
|
m.isOwner = false
|
||||||
|
m.ownerLock.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
m.currentSource = source
|
m.currentSource = source
|
||||||
m.sourceMutex.Lock()
|
m.sourceMutex.Lock()
|
||||||
m.sourceMimeTypes = []string{mimeType}
|
m.sourceMimeTypes = []string{mimeType}
|
||||||
m.sourceMutex.Unlock()
|
m.sourceMutex.Unlock()
|
||||||
|
|
||||||
|
m.ownerLock.Lock()
|
||||||
|
m.isOwner = true
|
||||||
|
m.ownerLock.Unlock()
|
||||||
|
|
||||||
device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1)
|
device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1)
|
||||||
if err := device.SetSelection(source); err != nil {
|
if err := device.SetSelection(source); err != nil {
|
||||||
log.Errorf("Failed to set selection: %v", err)
|
log.Errorf("Failed to set selection: %v", err)
|
||||||
@@ -1291,6 +1398,8 @@ func (m *Manager) StoreData(data []byte, mimeType string) error {
|
|||||||
switch {
|
switch {
|
||||||
case entry.IsImage:
|
case entry.IsImage:
|
||||||
entry.Preview = m.imagePreview(data, mimeType)
|
entry.Preview = m.imagePreview(data, mimeType)
|
||||||
|
case mimeType == "text/uri-list":
|
||||||
|
entry.Preview, entry.IsImage = m.uriListPreview(data)
|
||||||
default:
|
default:
|
||||||
entry.Preview = m.textPreview(data)
|
entry.Preview = m.textPreview(data)
|
||||||
}
|
}
|
||||||
@@ -1454,3 +1563,233 @@ func (m *Manager) GetPinnedCount() int {
|
|||||||
|
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) CopyFile(filePath string) error {
|
||||||
|
fileInfo, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("file not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := m.getConfig()
|
||||||
|
if fileInfo.Size() > cfg.MaxEntrySize {
|
||||||
|
return fmt.Errorf("file too large: %d > %d", fileInfo.Size(), cfg.MaxEntrySize)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileData, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exportedPath, err := m.ExportFileForFlatpak(filePath)
|
||||||
|
if err != nil {
|
||||||
|
exportedPath = filePath
|
||||||
|
}
|
||||||
|
fileURI := "file://" + exportedPath
|
||||||
|
|
||||||
|
if imgData, imgMime, ok := m.tryReadImageFromURI([]byte("file://" + filePath)); ok {
|
||||||
|
entry := Entry{
|
||||||
|
Data: imgData,
|
||||||
|
MimeType: imgMime,
|
||||||
|
Size: len(imgData),
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
IsImage: true,
|
||||||
|
Preview: m.imagePreview(imgData, imgMime),
|
||||||
|
}
|
||||||
|
if err := m.storeEntry(entry); err != nil {
|
||||||
|
log.Errorf("Failed to store file entry: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entry := Entry{
|
||||||
|
Data: fileData,
|
||||||
|
MimeType: "text/uri-list",
|
||||||
|
Size: len(fileData),
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
IsImage: false,
|
||||||
|
Preview: fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)),
|
||||||
|
}
|
||||||
|
if err := m.storeEntry(entry); err != nil {
|
||||||
|
log.Errorf("Failed to store file entry: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.updateState()
|
||||||
|
m.notifySubscribers()
|
||||||
|
|
||||||
|
m.post(func() {
|
||||||
|
if m.dataControlMgr == nil || m.dataDevice == nil {
|
||||||
|
log.Error("Data control manager or device not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dataMgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1)
|
||||||
|
source, err := dataMgr.CreateDataSource()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to create data source: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type offer struct {
|
||||||
|
mime string
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
offers := []offer{
|
||||||
|
{"x-special/gnome-copied-files", []byte("copy\n" + fileURI)},
|
||||||
|
{"text/uri-list", []byte(fileURI + "\r\n")},
|
||||||
|
{"text/plain", []byte(filePath)},
|
||||||
|
}
|
||||||
|
|
||||||
|
offerData := make(map[string][]byte)
|
||||||
|
for _, o := range offers {
|
||||||
|
if err := source.Offer(o.mime); err != nil {
|
||||||
|
log.Errorf("Failed to offer %s: %v", o.mime, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
offerData[o.mime] = o.data
|
||||||
|
}
|
||||||
|
|
||||||
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
|
fd := e.Fd
|
||||||
|
defer syscall.Close(fd)
|
||||||
|
file := os.NewFile(uintptr(fd), "clipboard-pipe")
|
||||||
|
defer file.Close()
|
||||||
|
if data, ok := offerData[e.MimeType]; ok {
|
||||||
|
file.Write(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
||||||
|
m.ownerLock.Lock()
|
||||||
|
m.isOwner = false
|
||||||
|
m.ownerLock.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
m.currentSource = source
|
||||||
|
|
||||||
|
m.ownerLock.Lock()
|
||||||
|
m.isOwner = true
|
||||||
|
m.ownerLock.Unlock()
|
||||||
|
|
||||||
|
device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1)
|
||||||
|
if err := device.SetSelection(source); err != nil {
|
||||||
|
log.Errorf("Failed to set selection: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) EntryToFile(entry *Entry) string {
|
||||||
|
switch {
|
||||||
|
case entry.MimeType == "text/uri-list":
|
||||||
|
data := strings.TrimSpace(string(entry.Data))
|
||||||
|
lines := strings.Split(data, "\n")
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
uri := strings.TrimSuffix(strings.TrimSpace(lines[0]), "\r")
|
||||||
|
if path, ok := strings.CutPrefix(uri, "file://"); ok {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
case entry.IsImage:
|
||||||
|
ext := ".png"
|
||||||
|
if suffix, ok := strings.CutPrefix(entry.MimeType, "image/"); ok {
|
||||||
|
ext = "." + suffix
|
||||||
|
}
|
||||||
|
cacheDir, err := os.UserCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
clipDir := filepath.Join(cacheDir, "dms", "clipboard")
|
||||||
|
if err := os.MkdirAll(clipDir, 0o755); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
filePath := filepath.Join(clipDir, fmt.Sprintf("%d%s", time.Now().UnixNano(), ext))
|
||||||
|
if os.WriteFile(filePath, entry.Data, 0o644) != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ExportFileForFlatpak(filePath string) (string, error) {
|
||||||
|
if _, err := os.Stat(filePath); err != nil {
|
||||||
|
return "", fmt.Errorf("file not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.dbusConn == nil {
|
||||||
|
conn, err := dbus.ConnectSessionBus()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("connect session bus: %w", err)
|
||||||
|
}
|
||||||
|
if !conn.SupportsUnixFDs() {
|
||||||
|
conn.Close()
|
||||||
|
return "", fmt.Errorf("D-Bus connection does not support Unix FD passing")
|
||||||
|
}
|
||||||
|
m.dbusConn = conn
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("open file: %w", err)
|
||||||
|
}
|
||||||
|
fd := int(file.Fd())
|
||||||
|
|
||||||
|
portal := m.dbusConn.Object("org.freedesktop.portal.Documents", "/org/freedesktop/portal/documents")
|
||||||
|
|
||||||
|
var docIds []string
|
||||||
|
var extra map[string]dbus.Variant
|
||||||
|
flags := uint32(0)
|
||||||
|
|
||||||
|
err = portal.Call(
|
||||||
|
"org.freedesktop.portal.Documents.AddFull",
|
||||||
|
0,
|
||||||
|
[]dbus.UnixFD{dbus.UnixFD(fd)},
|
||||||
|
flags,
|
||||||
|
"",
|
||||||
|
[]string{},
|
||||||
|
).Store(&docIds, &extra)
|
||||||
|
|
||||||
|
file.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("AddFull: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(docIds) == 0 {
|
||||||
|
return "", fmt.Errorf("no doc IDs returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
docId := docIds[0]
|
||||||
|
|
||||||
|
for _, app := range getInstalledFlatpaks() {
|
||||||
|
_ = portal.Call(
|
||||||
|
"org.freedesktop.portal.Documents.GrantPermissions",
|
||||||
|
0,
|
||||||
|
docId,
|
||||||
|
app,
|
||||||
|
[]string{"read"},
|
||||||
|
).Err
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := os.Getuid()
|
||||||
|
basename := filepath.Base(filePath)
|
||||||
|
exportedPath := fmt.Sprintf("/run/user/%d/doc/%s/%s", uid, docId, basename)
|
||||||
|
|
||||||
|
return exportedPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInstalledFlatpaks() []string {
|
||||||
|
out, err := exec.Command("flatpak", "list", "--app", "--columns=application").Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var apps []string
|
||||||
|
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||||
|
if app := strings.TrimSpace(line); app != "" {
|
||||||
|
apps = append(apps, app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apps
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
||||||
@@ -65,7 +66,7 @@ func SaveConfig(cfg Config) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ func SaveConfig(cfg Config) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(path, data, 0644)
|
return os.WriteFile(path, data, 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchParams struct {
|
type SearchParams struct {
|
||||||
@@ -157,6 +158,8 @@ type Manager struct {
|
|||||||
dirty chan struct{}
|
dirty chan struct{}
|
||||||
notifierWg sync.WaitGroup
|
notifierWg sync.WaitGroup
|
||||||
lastState *State
|
lastState *State
|
||||||
|
|
||||||
|
dbusConn *dbus.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) GetState() State {
|
func (m *Manager) GetState() State {
|
||||||
|
|||||||
@@ -60,12 +60,44 @@ func (m *Manager) Call(bus, dest, path, iface, method string, args []any) (*Call
|
|||||||
obj := conn.Object(dest, dbus.ObjectPath(path))
|
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||||
fullMethod := iface + "." + method
|
fullMethod := iface + "." + method
|
||||||
|
|
||||||
call := obj.Call(fullMethod, 0, args...)
|
convertedArgs := convertArgs(args)
|
||||||
|
call := obj.Call(fullMethod, 0, convertedArgs...)
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
return nil, fmt.Errorf("dbus call failed: %w", call.Err)
|
return nil, fmt.Errorf("dbus call failed: %w", call.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CallResult{Values: call.Body}, nil
|
return &CallResult{Values: dbusutil.NormalizeSlice(call.Body)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertArgs(args []any) []any {
|
||||||
|
result := make([]any, len(args))
|
||||||
|
for i, arg := range args {
|
||||||
|
result[i] = convertArg(arg)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertArg(arg any) any {
|
||||||
|
switch v := arg.(type) {
|
||||||
|
case float64:
|
||||||
|
if v == float64(uint32(v)) && v >= 0 && v <= float64(^uint32(0)) {
|
||||||
|
return uint32(v)
|
||||||
|
}
|
||||||
|
if v == float64(int32(v)) {
|
||||||
|
return int32(v)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
case []any:
|
||||||
|
return convertArgs(v)
|
||||||
|
case map[string]any:
|
||||||
|
result := make(map[string]any)
|
||||||
|
for k, val := range v {
|
||||||
|
result[k] = convertArg(val)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
default:
|
||||||
|
return arg
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) GetProperty(bus, dest, path, iface, property string) (*PropertyResult, error) {
|
func (m *Manager) GetProperty(bus, dest, path, iface, property string) (*PropertyResult, error) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||||
serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins"
|
serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
||||||
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
|
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
||||||
@@ -44,6 +45,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(req.Method, "theme.auto.") {
|
||||||
|
if themeModeManager == nil {
|
||||||
|
models.RespondError(conn, req.ID, "theme mode manager not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
thememode.HandleRequest(conn, req, themeModeManager)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(req.Method, "loginctl.") {
|
if strings.HasPrefix(req.Method, "loginctl.") {
|
||||||
if loginctlManager == nil {
|
if loginctlManager == nil {
|
||||||
models.RespondError(conn, req.ID, "loginctl manager not initialized")
|
models.RespondError(conn, req.ID, "loginctl manager not initialized")
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
||||||
@@ -68,6 +69,7 @@ var evdevManager *evdev.Manager
|
|||||||
var clipboardManager *clipboard.Manager
|
var clipboardManager *clipboard.Manager
|
||||||
var dbusManager *serverDbus.Manager
|
var dbusManager *serverDbus.Manager
|
||||||
var wlContext *wlcontext.SharedContext
|
var wlContext *wlcontext.SharedContext
|
||||||
|
var themeModeManager *thememode.Manager
|
||||||
|
|
||||||
const dbusClientID = "dms-dbus-client"
|
const dbusClientID = "dms-dbus-client"
|
||||||
|
|
||||||
@@ -380,6 +382,14 @@ func InitializeDbusManager() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func InitializeThemeModeManager() error {
|
||||||
|
manager := thememode.NewManager()
|
||||||
|
themeModeManager = manager
|
||||||
|
|
||||||
|
log.Info("Theme mode automation manager initialized")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func handleConnection(conn net.Conn) {
|
func handleConnection(conn net.Conn) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
@@ -457,6 +467,10 @@ func getCapabilities() Capabilities {
|
|||||||
caps = append(caps, "clipboard")
|
caps = append(caps, "clipboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if themeModeManager != nil {
|
||||||
|
caps = append(caps, "theme.auto")
|
||||||
|
}
|
||||||
|
|
||||||
if dbusManager != nil {
|
if dbusManager != nil {
|
||||||
caps = append(caps, "dbus")
|
caps = append(caps, "dbus")
|
||||||
}
|
}
|
||||||
@@ -519,6 +533,10 @@ func getServerInfo() ServerInfo {
|
|||||||
caps = append(caps, "clipboard")
|
caps = append(caps, "clipboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if themeModeManager != nil {
|
||||||
|
caps = append(caps, "theme.auto")
|
||||||
|
}
|
||||||
|
|
||||||
if dbusManager != nil {
|
if dbusManager != nil {
|
||||||
caps = append(caps, "dbus")
|
caps = append(caps, "dbus")
|
||||||
}
|
}
|
||||||
@@ -791,6 +809,38 @@ func handleSubscribe(conn net.Conn, req models.Request) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if shouldSubscribe("theme.auto") && themeModeManager != nil {
|
||||||
|
wg.Add(1)
|
||||||
|
themeAutoChan := themeModeManager.Subscribe(clientID + "-theme-auto")
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer themeModeManager.Unsubscribe(clientID + "-theme-auto")
|
||||||
|
|
||||||
|
initialState := themeModeManager.GetState()
|
||||||
|
select {
|
||||||
|
case eventChan <- ServiceEvent{Service: "theme.auto", Data: initialState}:
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case state, ok := <-themeAutoChan:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case eventChan <- ServiceEvent{Service: "theme.auto", Data: state}:
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
if shouldSubscribe("bluetooth") && bluezManager != nil {
|
if shouldSubscribe("bluetooth") && bluezManager != nil {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
bluezChan := bluezManager.Subscribe(clientID + "-bluetooth")
|
bluezChan := bluezManager.Subscribe(clientID + "-bluetooth")
|
||||||
@@ -1251,6 +1301,9 @@ func cleanupManagers() {
|
|||||||
if dbusManager != nil {
|
if dbusManager != nil {
|
||||||
dbusManager.Close()
|
dbusManager.Close()
|
||||||
}
|
}
|
||||||
|
if themeModeManager != nil {
|
||||||
|
themeModeManager.Close()
|
||||||
|
}
|
||||||
if wlContext != nil {
|
if wlContext != nil {
|
||||||
wlContext.Close()
|
wlContext.Close()
|
||||||
}
|
}
|
||||||
@@ -1346,6 +1399,15 @@ func Start(printDocs bool) error {
|
|||||||
log.Info(" wayland.gamma.setGamma - Set gamma value (params: gamma)")
|
log.Info(" wayland.gamma.setGamma - Set gamma value (params: gamma)")
|
||||||
log.Info(" wayland.gamma.setEnabled - Enable/disable gamma control (params: enabled)")
|
log.Info(" wayland.gamma.setEnabled - Enable/disable gamma control (params: enabled)")
|
||||||
log.Info(" wayland.gamma.subscribe - Subscribe to gamma state changes (streaming)")
|
log.Info(" wayland.gamma.subscribe - Subscribe to gamma state changes (streaming)")
|
||||||
|
log.Info("Theme automation:")
|
||||||
|
log.Info(" theme.auto.getState - Get current theme automation state")
|
||||||
|
log.Info(" theme.auto.setEnabled - Enable/disable theme automation (params: enabled)")
|
||||||
|
log.Info(" theme.auto.setMode - Set automation mode (params: mode [time|location])")
|
||||||
|
log.Info(" theme.auto.setSchedule - Set time schedule (params: startHour, startMinute, endHour, endMinute)")
|
||||||
|
log.Info(" theme.auto.setLocation - Set location (params: latitude, longitude)")
|
||||||
|
log.Info(" theme.auto.setUseIPLocation - Use IP location (params: use)")
|
||||||
|
log.Info(" theme.auto.trigger - Trigger immediate re-evaluation")
|
||||||
|
log.Info(" theme.auto.subscribe - Subscribe to theme automation state changes (streaming)")
|
||||||
log.Info("Bluetooth:")
|
log.Info("Bluetooth:")
|
||||||
log.Info(" bluetooth.getState - Get current bluetooth state")
|
log.Info(" bluetooth.getState - Get current bluetooth state")
|
||||||
log.Info(" bluetooth.startDiscovery - Start device discovery")
|
log.Info(" bluetooth.startDiscovery - Start device discovery")
|
||||||
@@ -1503,6 +1565,12 @@ func Start(printDocs bool) error {
|
|||||||
log.Debugf("WlrOutput manager unavailable: %v", err)
|
log.Debugf("WlrOutput manager unavailable: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := InitializeThemeModeManager(); err != nil {
|
||||||
|
log.Warnf("Theme mode manager unavailable: %v", err)
|
||||||
|
} else {
|
||||||
|
notifyCapabilityChange()
|
||||||
|
}
|
||||||
|
|
||||||
fatalErrChan := make(chan error, 1)
|
fatalErrChan := make(chan error, 1)
|
||||||
if wlrOutputManager != nil {
|
if wlrOutputManager != nil {
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -163,11 +163,11 @@ func TestCleanupStaleSockets(t *testing.T) {
|
|||||||
t.Setenv("XDG_RUNTIME_DIR", tempDir)
|
t.Setenv("XDG_RUNTIME_DIR", tempDir)
|
||||||
|
|
||||||
staleSocket := filepath.Join(tempDir, "danklinux-999999.sock")
|
staleSocket := filepath.Join(tempDir, "danklinux-999999.sock")
|
||||||
err := os.WriteFile(staleSocket, []byte{}, 0600)
|
err := os.WriteFile(staleSocket, []byte{}, 0o600)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
activeSocket := filepath.Join(tempDir, fmt.Sprintf("danklinux-%d.sock", os.Getpid()))
|
activeSocket := filepath.Join(tempDir, fmt.Sprintf("danklinux-%d.sock", os.Getpid()))
|
||||||
err = os.WriteFile(activeSocket, []byte{}, 0600)
|
err = os.WriteFile(activeSocket, []byte{}, 0o600)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cleanupStaleSockets()
|
cleanupStaleSockets()
|
||||||
|
|||||||
154
core/internal/server/thememode/handlers.go
Normal file
154
core/internal/server/thememode/handlers.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package thememode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
if manager == nil {
|
||||||
|
models.RespondError(conn, req.ID, "theme mode manager not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.Method {
|
||||||
|
case "theme.auto.getState":
|
||||||
|
handleGetState(conn, req, manager)
|
||||||
|
case "theme.auto.setEnabled":
|
||||||
|
handleSetEnabled(conn, req, manager)
|
||||||
|
case "theme.auto.setMode":
|
||||||
|
handleSetMode(conn, req, manager)
|
||||||
|
case "theme.auto.setSchedule":
|
||||||
|
handleSetSchedule(conn, req, manager)
|
||||||
|
case "theme.auto.setLocation":
|
||||||
|
handleSetLocation(conn, req, manager)
|
||||||
|
case "theme.auto.setUseIPLocation":
|
||||||
|
handleSetUseIPLocation(conn, req, manager)
|
||||||
|
case "theme.auto.trigger":
|
||||||
|
handleTrigger(conn, req, manager)
|
||||||
|
case "theme.auto.subscribe":
|
||||||
|
handleSubscribe(conn, req, manager)
|
||||||
|
default:
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
models.Respond(conn, req.ID, manager.GetState())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetEnabled(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
enabled, err := params.Bool(req.Params, "enabled")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetEnabled(enabled)
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto enabled set"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetMode(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
mode, err := params.String(req.Params, "mode")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode != "time" && mode != "location" {
|
||||||
|
models.RespondError(conn, req.ID, "invalid mode")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetMode(mode)
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto mode set"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetSchedule(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
startHour, err := params.Int(req.Params, "startHour")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startMinute, err := params.Int(req.Params, "startMinute")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
endHour, err := params.Int(req.Params, "endHour")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
endMinute, err := params.Int(req.Params, "endMinute")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := manager.ValidateSchedule(startHour, startMinute, endHour, endMinute); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetSchedule(startHour, startMinute, endHour, endMinute)
|
||||||
|
models.Respond(conn, req.ID, manager.GetState())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
lat, err := params.Float(req.Params, "latitude")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lon, err := params.Float(req.Params, "longitude")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetLocation(lat, lon)
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto location set"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetUseIPLocation(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
use, err := params.Bool(req.Params, "use")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetUseIPLocation(use)
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto IP location set"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTrigger(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
manager.TriggerUpdate()
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto update triggered"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
clientID := fmt.Sprintf("client-%p", conn)
|
||||||
|
stateChan := manager.Subscribe(clientID)
|
||||||
|
defer manager.Unsubscribe(clientID)
|
||||||
|
|
||||||
|
initialState := manager.GetState()
|
||||||
|
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||||
|
ID: req.ID,
|
||||||
|
Result: &initialState,
|
||||||
|
}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for state := range stateChan {
|
||||||
|
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||||
|
Result: &state,
|
||||||
|
}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
432
core/internal/server/thememode/manager.go
Normal file
432
core/internal/server/thememode/manager.go
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
package thememode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultStartHour = 18
|
||||||
|
defaultStartMinute = 0
|
||||||
|
defaultEndHour = 6
|
||||||
|
defaultEndMinute = 0
|
||||||
|
defaultElevationTwilight = -6.0
|
||||||
|
defaultElevationDaylight = 3.0
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
config Config
|
||||||
|
configMutex sync.RWMutex
|
||||||
|
|
||||||
|
state *State
|
||||||
|
stateMutex sync.RWMutex
|
||||||
|
|
||||||
|
subscribers syncmap.Map[string, chan State]
|
||||||
|
|
||||||
|
locationMutex sync.RWMutex
|
||||||
|
cachedIPLat *float64
|
||||||
|
cachedIPLon *float64
|
||||||
|
|
||||||
|
stopChan chan struct{}
|
||||||
|
updateTrigger chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() *Manager {
|
||||||
|
m := &Manager{
|
||||||
|
config: Config{
|
||||||
|
Enabled: false,
|
||||||
|
Mode: "time",
|
||||||
|
StartHour: defaultStartHour,
|
||||||
|
StartMinute: defaultStartMinute,
|
||||||
|
EndHour: defaultEndHour,
|
||||||
|
EndMinute: defaultEndMinute,
|
||||||
|
ElevationTwilight: defaultElevationTwilight,
|
||||||
|
ElevationDaylight: defaultElevationDaylight,
|
||||||
|
},
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
updateTrigger: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
m.updateState(time.Now())
|
||||||
|
|
||||||
|
m.wg.Add(1)
|
||||||
|
go m.schedulerLoop()
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetState() State {
|
||||||
|
m.stateMutex.RLock()
|
||||||
|
defer m.stateMutex.RUnlock()
|
||||||
|
if m.state == nil {
|
||||||
|
return State{Config: m.getConfig()}
|
||||||
|
}
|
||||||
|
stateCopy := *m.state
|
||||||
|
return stateCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Subscribe(id string) chan State {
|
||||||
|
ch := make(chan State, 64)
|
||||||
|
m.subscribers.Store(id, ch)
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Unsubscribe(id string) {
|
||||||
|
if val, ok := m.subscribers.LoadAndDelete(id); ok {
|
||||||
|
close(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetEnabled(enabled bool) {
|
||||||
|
m.configMutex.Lock()
|
||||||
|
if m.config.Enabled == enabled {
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.config.Enabled = enabled
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
m.TriggerUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetMode(mode string) {
|
||||||
|
m.configMutex.Lock()
|
||||||
|
if m.config.Mode == mode {
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.config.Mode = mode
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
m.TriggerUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetSchedule(startHour, startMinute, endHour, endMinute int) {
|
||||||
|
m.configMutex.Lock()
|
||||||
|
changed := m.config.StartHour != startHour ||
|
||||||
|
m.config.StartMinute != startMinute ||
|
||||||
|
m.config.EndHour != endHour ||
|
||||||
|
m.config.EndMinute != endMinute
|
||||||
|
if !changed {
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.config.StartHour = startHour
|
||||||
|
m.config.StartMinute = startMinute
|
||||||
|
m.config.EndHour = endHour
|
||||||
|
m.config.EndMinute = endMinute
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
m.TriggerUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetLocation(lat, lon float64) {
|
||||||
|
m.configMutex.Lock()
|
||||||
|
if m.config.Latitude != nil && m.config.Longitude != nil &&
|
||||||
|
*m.config.Latitude == lat && *m.config.Longitude == lon && !m.config.UseIPLocation {
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.config.Latitude = &lat
|
||||||
|
m.config.Longitude = &lon
|
||||||
|
m.config.UseIPLocation = false
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
|
||||||
|
m.locationMutex.Lock()
|
||||||
|
m.cachedIPLat = nil
|
||||||
|
m.cachedIPLon = nil
|
||||||
|
m.locationMutex.Unlock()
|
||||||
|
|
||||||
|
m.TriggerUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetUseIPLocation(use bool) {
|
||||||
|
m.configMutex.Lock()
|
||||||
|
if m.config.UseIPLocation == use {
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.config.UseIPLocation = use
|
||||||
|
if use {
|
||||||
|
m.config.Latitude = nil
|
||||||
|
m.config.Longitude = nil
|
||||||
|
}
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
|
||||||
|
if use {
|
||||||
|
m.locationMutex.Lock()
|
||||||
|
m.cachedIPLat = nil
|
||||||
|
m.cachedIPLon = nil
|
||||||
|
m.locationMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
m.TriggerUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) TriggerUpdate() {
|
||||||
|
select {
|
||||||
|
case m.updateTrigger <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Close() {
|
||||||
|
select {
|
||||||
|
case <-m.stopChan:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
close(m.stopChan)
|
||||||
|
}
|
||||||
|
m.wg.Wait()
|
||||||
|
m.subscribers.Range(func(key string, ch chan State) bool {
|
||||||
|
close(ch)
|
||||||
|
m.subscribers.Delete(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) schedulerLoop() {
|
||||||
|
defer m.wg.Done()
|
||||||
|
|
||||||
|
var timer *time.Timer
|
||||||
|
for {
|
||||||
|
config := m.getConfig()
|
||||||
|
now := time.Now()
|
||||||
|
var isLight bool
|
||||||
|
var next time.Time
|
||||||
|
if config.Enabled {
|
||||||
|
isLight, next = m.computeSchedule(now, config)
|
||||||
|
} else {
|
||||||
|
m.stateMutex.RLock()
|
||||||
|
if m.state != nil {
|
||||||
|
isLight = m.state.IsLight
|
||||||
|
}
|
||||||
|
m.stateMutex.RUnlock()
|
||||||
|
next = now.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.updateStateWithValues(config, isLight, next)
|
||||||
|
|
||||||
|
waitDur := time.Until(next)
|
||||||
|
if !config.Enabled {
|
||||||
|
waitDur = 24 * time.Hour
|
||||||
|
}
|
||||||
|
if waitDur < time.Second {
|
||||||
|
waitDur = time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
if timer != nil {
|
||||||
|
timer.Stop()
|
||||||
|
}
|
||||||
|
timer = time.NewTimer(waitDur)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-m.stopChan:
|
||||||
|
timer.Stop()
|
||||||
|
return
|
||||||
|
case <-m.updateTrigger:
|
||||||
|
timer.Stop()
|
||||||
|
continue
|
||||||
|
case <-timer.C:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) updateState(now time.Time) {
|
||||||
|
config := m.getConfig()
|
||||||
|
var isLight bool
|
||||||
|
var next time.Time
|
||||||
|
if config.Enabled {
|
||||||
|
isLight, next = m.computeSchedule(now, config)
|
||||||
|
} else {
|
||||||
|
m.stateMutex.RLock()
|
||||||
|
if m.state != nil {
|
||||||
|
isLight = m.state.IsLight
|
||||||
|
}
|
||||||
|
m.stateMutex.RUnlock()
|
||||||
|
next = now.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
m.updateStateWithValues(config, isLight, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) updateStateWithValues(config Config, isLight bool, next time.Time) {
|
||||||
|
newState := State{
|
||||||
|
Config: config,
|
||||||
|
IsLight: isLight,
|
||||||
|
NextTransition: next,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.stateMutex.Lock()
|
||||||
|
if m.state != nil && statesEqual(m.state, &newState) {
|
||||||
|
m.stateMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.state = &newState
|
||||||
|
m.stateMutex.Unlock()
|
||||||
|
|
||||||
|
m.notifySubscribers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) notifySubscribers() {
|
||||||
|
state := m.GetState()
|
||||||
|
m.subscribers.Range(func(key string, ch chan State) bool {
|
||||||
|
select {
|
||||||
|
case ch <- state:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getConfig() Config {
|
||||||
|
m.configMutex.RLock()
|
||||||
|
defer m.configMutex.RUnlock()
|
||||||
|
return m.config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
||||||
|
if config.Latitude != nil && config.Longitude != nil {
|
||||||
|
return config.Latitude, config.Longitude
|
||||||
|
}
|
||||||
|
if !config.UseIPLocation {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.locationMutex.RLock()
|
||||||
|
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
||||||
|
lat, lon := m.cachedIPLat, m.cachedIPLon
|
||||||
|
m.locationMutex.RUnlock()
|
||||||
|
return lat, lon
|
||||||
|
}
|
||||||
|
m.locationMutex.RUnlock()
|
||||||
|
|
||||||
|
lat, lon, err := wayland.FetchIPLocation()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.locationMutex.Lock()
|
||||||
|
m.cachedIPLat = lat
|
||||||
|
m.cachedIPLon = lon
|
||||||
|
m.locationMutex.Unlock()
|
||||||
|
|
||||||
|
return lat, lon
|
||||||
|
}
|
||||||
|
|
||||||
|
func statesEqual(a, b *State) bool {
|
||||||
|
if a == nil || b == nil {
|
||||||
|
return a == b
|
||||||
|
}
|
||||||
|
if a.IsLight != b.IsLight || !a.NextTransition.Equal(b.NextTransition) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return a.Config == b.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) {
|
||||||
|
if config.Mode == "location" {
|
||||||
|
return m.computeLocationSchedule(now, config)
|
||||||
|
}
|
||||||
|
return computeTimeSchedule(now, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) {
|
||||||
|
startMinutes := config.StartHour*60 + config.StartMinute
|
||||||
|
endMinutes := config.EndHour*60 + config.EndMinute
|
||||||
|
currentMinutes := now.Hour()*60 + now.Minute()
|
||||||
|
|
||||||
|
startTime := time.Date(now.Year(), now.Month(), now.Day(), config.StartHour, config.StartMinute, 0, 0, now.Location())
|
||||||
|
endTime := time.Date(now.Year(), now.Month(), now.Day(), config.EndHour, config.EndMinute, 0, 0, now.Location())
|
||||||
|
|
||||||
|
if startMinutes == endMinutes {
|
||||||
|
next := startTime
|
||||||
|
if !next.After(now) {
|
||||||
|
next = next.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
return true, next
|
||||||
|
}
|
||||||
|
|
||||||
|
if startMinutes < endMinutes {
|
||||||
|
if currentMinutes < startMinutes {
|
||||||
|
return true, startTime
|
||||||
|
}
|
||||||
|
if currentMinutes >= endMinutes {
|
||||||
|
return true, startTime.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
return false, endTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentMinutes >= startMinutes {
|
||||||
|
return false, endTime.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
if currentMinutes < endMinutes {
|
||||||
|
return false, endTime
|
||||||
|
}
|
||||||
|
return true, startTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, time.Time) {
|
||||||
|
lat, lon := m.getLocation(config)
|
||||||
|
if lat == nil || lon == nil {
|
||||||
|
currentIsLight := false
|
||||||
|
m.stateMutex.RLock()
|
||||||
|
if m.state != nil {
|
||||||
|
currentIsLight = m.state.IsLight
|
||||||
|
}
|
||||||
|
m.stateMutex.RUnlock()
|
||||||
|
return currentIsLight, now.Add(10 * time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight)
|
||||||
|
if cond != wayland.SunNormal {
|
||||||
|
if cond == wayland.SunMidnightSun {
|
||||||
|
return true, startOfNextDay(now)
|
||||||
|
}
|
||||||
|
return false, startOfNextDay(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
if now.Before(times.Sunrise) {
|
||||||
|
return false, times.Sunrise
|
||||||
|
}
|
||||||
|
if now.Before(times.Sunset) {
|
||||||
|
return true, times.Sunset
|
||||||
|
}
|
||||||
|
|
||||||
|
nextDay := startOfNextDay(now)
|
||||||
|
nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight)
|
||||||
|
if nextCond != wayland.SunNormal {
|
||||||
|
if nextCond == wayland.SunMidnightSun {
|
||||||
|
return true, startOfNextDay(nextDay)
|
||||||
|
}
|
||||||
|
return false, startOfNextDay(nextDay)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nextTimes.Sunrise
|
||||||
|
}
|
||||||
|
|
||||||
|
func startOfNextDay(t time.Time) time.Time {
|
||||||
|
next := t.Add(24 * time.Hour)
|
||||||
|
return time.Date(next.Year(), next.Month(), next.Day(), 0, 0, 0, 0, next.Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateHourMinute(hour, minute int) bool {
|
||||||
|
if hour < 0 || hour > 23 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if minute < 0 || minute > 59 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error {
|
||||||
|
if !validateHourMinute(startHour, startMinute) || !validateHourMinute(endHour, endMinute) {
|
||||||
|
return errInvalidTime
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errInvalidTime = errors.New("invalid schedule time")
|
||||||
23
core/internal/server/thememode/types.go
Normal file
23
core/internal/server/thememode/types.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package thememode
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
StartHour int `json:"startHour"`
|
||||||
|
StartMinute int `json:"startMinute"`
|
||||||
|
EndHour int `json:"endHour"`
|
||||||
|
EndMinute int `json:"endMinute"`
|
||||||
|
Latitude *float64 `json:"latitude,omitempty"`
|
||||||
|
Longitude *float64 `json:"longitude,omitempty"`
|
||||||
|
UseIPLocation bool `json:"useIPLocation"`
|
||||||
|
ElevationTwilight float64 `json:"elevationTwilight"`
|
||||||
|
ElevationDaylight float64 `json:"elevationDaylight"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
Config Config `json:"config"`
|
||||||
|
IsLight bool `json:"isLight"`
|
||||||
|
NextTransition time.Time `json:"nextTransition"`
|
||||||
|
}
|
||||||
@@ -626,6 +626,7 @@ func (m *Manager) schedulerLoop() {
|
|||||||
m.schedule.calcDay = time.Time{}
|
m.schedule.calcDay = time.Time{}
|
||||||
m.scheduleMutex.Unlock()
|
m.scheduleMutex.Unlock()
|
||||||
m.recalcSchedule(time.Now())
|
m.recalcSchedule(time.Now())
|
||||||
|
m.updateStateFromSchedule()
|
||||||
m.configMutex.RLock()
|
m.configMutex.RLock()
|
||||||
enabled := m.config.Enabled
|
enabled := m.config.Enabled
|
||||||
m.configMutex.RUnlock()
|
m.configMutex.RUnlock()
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func (m *Manager) Install(theme Theme, registryThemeDir string) error {
|
|||||||
return fmt.Errorf("theme already installed: %s", theme.Name)
|
return fmt.Errorf("theme already installed: %s", theme.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.fs.MkdirAll(themeDir, 0755); err != nil {
|
if err := m.fs.MkdirAll(themeDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create theme directory: %w", err)
|
return fmt.Errorf("failed to create theme directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ func (m *Manager) Install(theme Theme, registryThemeDir string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
themePath := filepath.Join(themeDir, "theme.json")
|
themePath := filepath.Join(themeDir, "theme.json")
|
||||||
if err := afero.WriteFile(m.fs, themePath, data, 0644); err != nil {
|
if err := afero.WriteFile(m.fs, themePath, data, 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write theme file: %w", err)
|
return fmt.Errorf("failed to write theme file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ func (m *Manager) copyPreviewFiles(srcDir, dstDir string, theme Theme) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dstPath := filepath.Join(dstDir, preview)
|
dstPath := filepath.Join(dstDir, preview)
|
||||||
_ = afero.WriteFile(m.fs, dstPath, data, 0644)
|
_ = afero.WriteFile(m.fs, dstPath, data, 0o644)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ func (m *Manager) Update(theme Theme) error {
|
|||||||
return fmt.Errorf("failed to marshal theme: %w", err)
|
return fmt.Errorf("failed to marshal theme: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := afero.WriteFile(m.fs, themePath, data, 0644); err != nil {
|
if err := afero.WriteFile(m.fs, themePath, data, 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write theme file: %w", err)
|
return fmt.Errorf("failed to write theme file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ func (r *Registry) Update() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
|
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ func (r *Registry) Update() error {
|
|||||||
return fmt.Errorf("failed to remove corrupted registry: %w", err)
|
return fmt.Errorf("failed to remove corrupted registry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
|
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ func (m Model) tryFingerprint() tea.Cmd {
|
|||||||
askpassScript := filepath.Join(tmpDir, fmt.Sprintf("danklinux-fp-%d.sh", time.Now().UnixNano()))
|
askpassScript := filepath.Join(tmpDir, fmt.Sprintf("danklinux-fp-%d.sh", time.Now().UnixNano()))
|
||||||
|
|
||||||
scriptContent := "#!/bin/sh\nexit 1\n"
|
scriptContent := "#!/bin/sh\nexit 1\n"
|
||||||
if err := os.WriteFile(askpassScript, []byte(scriptContent), 0700); err != nil {
|
if err := os.WriteFile(askpassScript, []byte(scriptContent), 0o700); err != nil {
|
||||||
return passwordValidMsg{password: "", valid: false}
|
return passwordValidMsg{password: "", valid: false}
|
||||||
}
|
}
|
||||||
defer os.Remove(askpassScript)
|
defer os.Remove(askpassScript)
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func TestFlatpakExistsCommandFailure(t *testing.T) {
|
|||||||
fakeFlatpak := filepath.Join(tempDir, "flatpak")
|
fakeFlatpak := filepath.Join(tempDir, "flatpak")
|
||||||
|
|
||||||
script := "#!/bin/sh\nexit 1\n"
|
script := "#!/bin/sh\nexit 1\n"
|
||||||
err := os.WriteFile(fakeFlatpak, []byte(script), 0755)
|
err := os.WriteFile(fakeFlatpak, []byte(script), 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create fake flatpak: %v", err)
|
t.Fatalf("failed to create fake flatpak: %v", err)
|
||||||
}
|
}
|
||||||
@@ -168,7 +168,7 @@ func TestFlatpakSearchBySubstringCommandFailure(t *testing.T) {
|
|||||||
fakeFlatpak := filepath.Join(tempDir, "flatpak")
|
fakeFlatpak := filepath.Join(tempDir, "flatpak")
|
||||||
|
|
||||||
script := "#!/bin/sh\nexit 1\n"
|
script := "#!/bin/sh\nexit 1\n"
|
||||||
err := os.WriteFile(fakeFlatpak, []byte(script), 0755)
|
err := os.WriteFile(fakeFlatpak, []byte(script), 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create fake flatpak: %v", err)
|
t.Fatalf("failed to create fake flatpak: %v", err)
|
||||||
}
|
}
|
||||||
@@ -192,7 +192,7 @@ func TestFlatpakInstallationDirCommandFailure(t *testing.T) {
|
|||||||
fakeFlatpak := filepath.Join(tempDir, "flatpak")
|
fakeFlatpak := filepath.Join(tempDir, "flatpak")
|
||||||
|
|
||||||
script := "#!/bin/sh\nexit 1\n"
|
script := "#!/bin/sh\nexit 1\n"
|
||||||
err := os.WriteFile(fakeFlatpak, []byte(script), 0755)
|
err := os.WriteFile(fakeFlatpak, []byte(script), 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create fake flatpak: %v", err)
|
t.Fatalf("failed to create fake flatpak: %v", err)
|
||||||
}
|
}
|
||||||
@@ -220,7 +220,7 @@ if [ "$1" = "info" ] && [ "$2" = "app.exists.test" ]; then
|
|||||||
fi
|
fi
|
||||||
exit 1
|
exit 1
|
||||||
`
|
`
|
||||||
err := os.WriteFile(fakeFlatpak, []byte(script), 0755)
|
err := os.WriteFile(fakeFlatpak, []byte(script), 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create fake flatpak: %v", err)
|
t.Fatalf("failed to create fake flatpak: %v", err)
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ func TestAnyFlatpakExistsNoneExist(t *testing.T) {
|
|||||||
fakeFlatpak := filepath.Join(tempDir, "flatpak")
|
fakeFlatpak := filepath.Join(tempDir, "flatpak")
|
||||||
|
|
||||||
script := "#!/bin/sh\nexit 1\n"
|
script := "#!/bin/sh\nexit 1\n"
|
||||||
err := os.WriteFile(fakeFlatpak, []byte(script), 0755)
|
err := os.WriteFile(fakeFlatpak, []byte(script), 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create fake flatpak: %v", err)
|
t.Fatalf("failed to create fake flatpak: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ func TestGetDMSVersionInfo_Structure(t *testing.T) {
|
|||||||
// Create a temp directory with a fake DMS installation
|
// Create a temp directory with a fake DMS installation
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
os.MkdirAll(dmsPath, 0755)
|
os.MkdirAll(dmsPath, 0o755)
|
||||||
|
|
||||||
// Create a .git directory to simulate git installation
|
// Create a .git directory to simulate git installation
|
||||||
os.MkdirAll(filepath.Join(dmsPath, ".git"), 0755)
|
os.MkdirAll(filepath.Join(dmsPath, ".git"), 0o755)
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
originalHome := os.Getenv("HOME")
|
||||||
defer os.Setenv("HOME", originalHome)
|
defer os.Setenv("HOME", originalHome)
|
||||||
@@ -84,8 +84,8 @@ func TestGetDMSVersionInfo_Structure(t *testing.T) {
|
|||||||
func TestGetDMSVersionInfo_BranchVersion(t *testing.T) {
|
func TestGetDMSVersionInfo_BranchVersion(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
os.MkdirAll(dmsPath, 0755)
|
os.MkdirAll(dmsPath, 0o755)
|
||||||
os.MkdirAll(filepath.Join(dmsPath, ".git"), 0755)
|
os.MkdirAll(filepath.Join(dmsPath, ".git"), 0o755)
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
originalHome := os.Getenv("HOME")
|
||||||
defer os.Setenv("HOME", originalHome)
|
defer os.Setenv("HOME", originalHome)
|
||||||
@@ -116,8 +116,8 @@ func TestGetDMSVersionInfo_BranchVersion(t *testing.T) {
|
|||||||
func TestGetDMSVersionInfo_NoUpdate(t *testing.T) {
|
func TestGetDMSVersionInfo_NoUpdate(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
os.MkdirAll(dmsPath, 0755)
|
os.MkdirAll(dmsPath, 0o755)
|
||||||
os.MkdirAll(filepath.Join(dmsPath, ".git"), 0755)
|
os.MkdirAll(filepath.Join(dmsPath, ".git"), 0o755)
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
originalHome := os.Getenv("HOME")
|
||||||
defer os.Setenv("HOME", originalHome)
|
defer os.Setenv("HOME", originalHome)
|
||||||
@@ -157,7 +157,7 @@ func TestGetCurrentDMSVersion_GitTag(t *testing.T) {
|
|||||||
|
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
os.MkdirAll(dmsPath, 0755)
|
os.MkdirAll(dmsPath, 0o755)
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
originalHome := os.Getenv("HOME")
|
||||||
defer os.Setenv("HOME", originalHome)
|
defer os.Setenv("HOME", originalHome)
|
||||||
@@ -168,7 +168,7 @@ func TestGetCurrentDMSVersion_GitTag(t *testing.T) {
|
|||||||
exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run()
|
exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run()
|
||||||
|
|
||||||
testFile := filepath.Join(dmsPath, "test.txt")
|
testFile := filepath.Join(dmsPath, "test.txt")
|
||||||
os.WriteFile(testFile, []byte("test"), 0644)
|
os.WriteFile(testFile, []byte("test"), 0o644)
|
||||||
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||||
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||||
exec.Command("git", "-C", dmsPath, "tag", "v0.1.0").Run()
|
exec.Command("git", "-C", dmsPath, "tag", "v0.1.0").Run()
|
||||||
@@ -190,7 +190,7 @@ func TestGetCurrentDMSVersion_GitBranch(t *testing.T) {
|
|||||||
|
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
os.MkdirAll(dmsPath, 0755)
|
os.MkdirAll(dmsPath, 0o755)
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
originalHome := os.Getenv("HOME")
|
||||||
defer os.Setenv("HOME", originalHome)
|
defer os.Setenv("HOME", originalHome)
|
||||||
@@ -202,7 +202,7 @@ func TestGetCurrentDMSVersion_GitBranch(t *testing.T) {
|
|||||||
exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run()
|
exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run()
|
||||||
|
|
||||||
testFile := filepath.Join(dmsPath, "test.txt")
|
testFile := filepath.Join(dmsPath, "test.txt")
|
||||||
os.WriteFile(testFile, []byte("test"), 0644)
|
os.WriteFile(testFile, []byte("test"), 0o644)
|
||||||
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||||
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||||
|
|
||||||
|
|||||||
658
core/internal/windowrules/providers/hyprland_parser.go
Normal file
658
core/internal/windowrules/providers/hyprland_parser.go
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HyprlandWindowRule struct {
|
||||||
|
MatchClass string
|
||||||
|
MatchTitle string
|
||||||
|
MatchXWayland *bool
|
||||||
|
MatchFloating *bool
|
||||||
|
MatchFullscreen *bool
|
||||||
|
MatchPinned *bool
|
||||||
|
MatchInitialised *bool
|
||||||
|
Rule string
|
||||||
|
Value string
|
||||||
|
Source string
|
||||||
|
RawLine string
|
||||||
|
}
|
||||||
|
|
||||||
|
type HyprlandRulesParser struct {
|
||||||
|
configDir string
|
||||||
|
processedFiles map[string]bool
|
||||||
|
rules []HyprlandWindowRule
|
||||||
|
currentSource string
|
||||||
|
dmsRulesExists bool
|
||||||
|
dmsRulesIncluded bool
|
||||||
|
includeCount int
|
||||||
|
dmsIncludePos int
|
||||||
|
rulesAfterDMS int
|
||||||
|
dmsProcessed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHyprlandRulesParser(configDir string) *HyprlandRulesParser {
|
||||||
|
return &HyprlandRulesParser{
|
||||||
|
configDir: configDir,
|
||||||
|
processedFiles: make(map[string]bool),
|
||||||
|
rules: []HyprlandWindowRule{},
|
||||||
|
dmsIncludePos: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandRulesParser) Parse() ([]HyprlandWindowRule, error) {
|
||||||
|
expandedDir, err := utils.ExpandPath(p.configDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dmsRulesPath := filepath.Join(expandedDir, "dms", "windowrules.conf")
|
||||||
|
if _, err := os.Stat(dmsRulesPath); err == nil {
|
||||||
|
p.dmsRulesExists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(expandedDir, "hyprland.conf")
|
||||||
|
if err := p.parseFile(mainConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.dmsRulesExists && !p.dmsProcessed {
|
||||||
|
p.parseDMSRulesDirectly(dmsRulesPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.rules, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandRulesParser) parseDMSRulesDirectly(dmsRulesPath string) {
|
||||||
|
data, err := os.ReadFile(dmsRulesPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSource := p.currentSource
|
||||||
|
p.currentSource = dmsRulesPath
|
||||||
|
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
p.parseLine(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.currentSource = prevSource
|
||||||
|
p.dmsProcessed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandRulesParser) parseFile(filePath string) error {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.processedFiles[absPath] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p.processedFiles[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSource := p.currentSource
|
||||||
|
p.currentSource = absPath
|
||||||
|
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
|
||||||
|
if strings.HasPrefix(trimmed, "source") {
|
||||||
|
p.handleSource(trimmed, filepath.Dir(absPath))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
p.parseLine(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.currentSource = prevSource
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandRulesParser) handleSource(line string, baseDir string) {
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcePath := strings.TrimSpace(parts[1])
|
||||||
|
isDMSSource := sourcePath == "dms/windowrules.conf" || strings.HasSuffix(sourcePath, "/dms/windowrules.conf")
|
||||||
|
|
||||||
|
p.includeCount++
|
||||||
|
if isDMSSource {
|
||||||
|
p.dmsRulesIncluded = 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
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = p.parseFile(expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandRulesParser) parseLine(line string) {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
|
||||||
|
if strings.HasPrefix(trimmed, "windowrule") {
|
||||||
|
rule := p.parseWindowRuleLine(trimmed)
|
||||||
|
if rule != nil {
|
||||||
|
rule.Source = p.currentSource
|
||||||
|
p.rules = append(p.rules, *rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var windowRuleV2Regex = regexp.MustCompile(`^windowrulev?2?\s*=\s*(.+)$`)
|
||||||
|
|
||||||
|
func (p *HyprlandRulesParser) parseWindowRuleLine(line string) *HyprlandWindowRule {
|
||||||
|
matches := windowRuleV2Regex.FindStringSubmatch(line)
|
||||||
|
if len(matches) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
content := strings.TrimSpace(matches[1])
|
||||||
|
isV2 := strings.HasPrefix(line, "windowrulev2")
|
||||||
|
|
||||||
|
rule := &HyprlandWindowRule{
|
||||||
|
RawLine: line,
|
||||||
|
}
|
||||||
|
|
||||||
|
if isV2 {
|
||||||
|
p.parseWindowRuleV2(content, rule)
|
||||||
|
} else {
|
||||||
|
p.parseWindowRuleV1(content, rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandRulesParser) parseWindowRuleV1(content string, rule *HyprlandWindowRule) {
|
||||||
|
parts := strings.SplitN(content, ",", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.Rule = strings.TrimSpace(parts[0])
|
||||||
|
rule.MatchClass = strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandRulesParser) parseWindowRuleV2(content string, rule *HyprlandWindowRule) {
|
||||||
|
parts := strings.SplitN(content, ",", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleAndValue := strings.TrimSpace(parts[0])
|
||||||
|
matchPart := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
if idx := strings.Index(ruleAndValue, " "); idx > 0 {
|
||||||
|
rule.Rule = ruleAndValue[:idx]
|
||||||
|
rule.Value = strings.TrimSpace(ruleAndValue[idx+1:])
|
||||||
|
} else {
|
||||||
|
rule.Rule = ruleAndValue
|
||||||
|
}
|
||||||
|
|
||||||
|
matchPairs := strings.Split(matchPart, ",")
|
||||||
|
for _, pair := range matchPairs {
|
||||||
|
pair = strings.TrimSpace(pair)
|
||||||
|
if colonIdx := strings.Index(pair, ":"); colonIdx > 0 {
|
||||||
|
key := strings.TrimSpace(pair[:colonIdx])
|
||||||
|
value := strings.TrimSpace(pair[colonIdx+1:])
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "class":
|
||||||
|
rule.MatchClass = value
|
||||||
|
case "title":
|
||||||
|
rule.MatchTitle = value
|
||||||
|
case "xwayland":
|
||||||
|
b := value == "1" || value == "true"
|
||||||
|
rule.MatchXWayland = &b
|
||||||
|
case "floating":
|
||||||
|
b := value == "1" || value == "true"
|
||||||
|
rule.MatchFloating = &b
|
||||||
|
case "fullscreen":
|
||||||
|
b := value == "1" || value == "true"
|
||||||
|
rule.MatchFullscreen = &b
|
||||||
|
case "pinned":
|
||||||
|
b := value == "1" || value == "true"
|
||||||
|
rule.MatchPinned = &b
|
||||||
|
case "initialised", "initialized":
|
||||||
|
b := value == "1" || value == "true"
|
||||||
|
rule.MatchInitialised = &b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandRulesParser) HasDMSRulesIncluded() bool {
|
||||||
|
return p.dmsRulesIncluded
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandRulesParser) buildDMSStatus() *windowrules.DMSRulesStatus {
|
||||||
|
status := &windowrules.DMSRulesStatus{
|
||||||
|
Exists: p.dmsRulesExists,
|
||||||
|
Included: p.dmsRulesIncluded,
|
||||||
|
IncludePosition: p.dmsIncludePos,
|
||||||
|
TotalIncludes: p.includeCount,
|
||||||
|
RulesAfterDMS: p.rulesAfterDMS,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case !p.dmsRulesExists:
|
||||||
|
status.Effective = false
|
||||||
|
status.StatusMessage = "dms/windowrules.conf does not exist"
|
||||||
|
case !p.dmsRulesIncluded:
|
||||||
|
status.Effective = false
|
||||||
|
status.StatusMessage = "dms/windowrules.conf is not sourced in config"
|
||||||
|
case p.rulesAfterDMS > 0:
|
||||||
|
status.Effective = true
|
||||||
|
status.OverriddenBy = p.rulesAfterDMS
|
||||||
|
status.StatusMessage = "Some DMS rules may be overridden by config rules"
|
||||||
|
default:
|
||||||
|
status.Effective = true
|
||||||
|
status.StatusMessage = "DMS window rules are active"
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
type HyprlandRulesParseResult struct {
|
||||||
|
Rules []HyprlandWindowRule
|
||||||
|
DMSRulesIncluded bool
|
||||||
|
DMSStatus *windowrules.DMSRulesStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseHyprlandWindowRules(configDir string) (*HyprlandRulesParseResult, error) {
|
||||||
|
parser := NewHyprlandRulesParser(configDir)
|
||||||
|
rules, err := parser.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &HyprlandRulesParseResult{
|
||||||
|
Rules: rules,
|
||||||
|
DMSRulesIncluded: parser.HasDMSRulesIncluded(),
|
||||||
|
DMSStatus: parser.buildDMSStatus(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyHyprlandRuleAction(actions *windowrules.Actions, rule, value string) {
|
||||||
|
t := true
|
||||||
|
switch rule {
|
||||||
|
case "float":
|
||||||
|
actions.OpenFloating = &t
|
||||||
|
case "tile":
|
||||||
|
actions.Tile = &t
|
||||||
|
case "fullscreen":
|
||||||
|
actions.OpenFullscreen = &t
|
||||||
|
case "maximize":
|
||||||
|
actions.OpenMaximized = &t
|
||||||
|
case "nofocus":
|
||||||
|
actions.NoFocus = &t
|
||||||
|
case "noborder":
|
||||||
|
actions.NoBorder = &t
|
||||||
|
case "noshadow":
|
||||||
|
actions.NoShadow = &t
|
||||||
|
case "nodim":
|
||||||
|
actions.NoDim = &t
|
||||||
|
case "noblur":
|
||||||
|
actions.NoBlur = &t
|
||||||
|
case "noanim":
|
||||||
|
actions.NoAnim = &t
|
||||||
|
case "norounding":
|
||||||
|
actions.NoRounding = &t
|
||||||
|
case "pin":
|
||||||
|
actions.Pin = &t
|
||||||
|
case "opaque":
|
||||||
|
actions.Opaque = &t
|
||||||
|
case "forcergbx":
|
||||||
|
actions.ForcergbX = &t
|
||||||
|
case "opacity":
|
||||||
|
if f, err := strconv.ParseFloat(value, 64); err == nil {
|
||||||
|
actions.Opacity = &f
|
||||||
|
}
|
||||||
|
case "size":
|
||||||
|
actions.Size = value
|
||||||
|
case "move":
|
||||||
|
actions.Move = value
|
||||||
|
case "monitor":
|
||||||
|
actions.Monitor = value
|
||||||
|
case "workspace":
|
||||||
|
actions.Workspace = value
|
||||||
|
case "idleinhibit":
|
||||||
|
actions.Idleinhibit = value
|
||||||
|
case "rounding":
|
||||||
|
if i, err := strconv.Atoi(value); err == nil {
|
||||||
|
actions.CornerRadius = &i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertHyprlandRulesToWindowRules(hyprRules []HyprlandWindowRule) []windowrules.WindowRule {
|
||||||
|
result := make([]windowrules.WindowRule, 0, len(hyprRules))
|
||||||
|
for i, hr := range hyprRules {
|
||||||
|
wr := windowrules.WindowRule{
|
||||||
|
ID: strconv.Itoa(i),
|
||||||
|
Enabled: true,
|
||||||
|
Source: hr.Source,
|
||||||
|
MatchCriteria: windowrules.MatchCriteria{
|
||||||
|
AppID: hr.MatchClass,
|
||||||
|
Title: hr.MatchTitle,
|
||||||
|
XWayland: hr.MatchXWayland,
|
||||||
|
IsFloating: hr.MatchFloating,
|
||||||
|
Fullscreen: hr.MatchFullscreen,
|
||||||
|
Pinned: hr.MatchPinned,
|
||||||
|
Initialised: hr.MatchInitialised,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
applyHyprlandRuleAction(&wr.Actions, hr.Rule, hr.Value)
|
||||||
|
result = append(result, wr)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
type HyprlandWritableProvider struct {
|
||||||
|
configDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHyprlandWritableProvider(configDir string) *HyprlandWritableProvider {
|
||||||
|
return &HyprlandWritableProvider{configDir: configDir}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandWritableProvider) Name() string {
|
||||||
|
return "hyprland"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandWritableProvider) GetOverridePath() string {
|
||||||
|
expanded, _ := utils.ExpandPath(p.configDir)
|
||||||
|
return filepath.Join(expanded, "dms", "windowrules.conf")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandWritableProvider) GetRuleSet() (*windowrules.RuleSet, error) {
|
||||||
|
result, err := ParseHyprlandWindowRules(p.configDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &windowrules.RuleSet{
|
||||||
|
Title: "Hyprland Window Rules",
|
||||||
|
Provider: "hyprland",
|
||||||
|
Rules: ConvertHyprlandRulesToWindowRules(result.Rules),
|
||||||
|
DMSRulesIncluded: result.DMSRulesIncluded,
|
||||||
|
DMSStatus: result.DMSStatus,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandWritableProvider) SetRule(rule windowrules.WindowRule) error {
|
||||||
|
rules, err := p.LoadDMSRules()
|
||||||
|
if err != nil {
|
||||||
|
rules = []windowrules.WindowRule{}
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for i, r := range rules {
|
||||||
|
if r.ID == rule.ID {
|
||||||
|
rules[i] = rule
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
rules = append(rules, rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.writeDMSRules(rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandWritableProvider) RemoveRule(id string) error {
|
||||||
|
rules, err := p.LoadDMSRules()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newRules := make([]windowrules.WindowRule, 0, len(rules))
|
||||||
|
for _, r := range rules {
|
||||||
|
if r.ID != id {
|
||||||
|
newRules = append(newRules, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.writeDMSRules(newRules)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandWritableProvider) ReorderRules(ids []string) error {
|
||||||
|
rules, err := p.LoadDMSRules()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleMap := make(map[string]windowrules.WindowRule)
|
||||||
|
for _, r := range rules {
|
||||||
|
ruleMap[r.ID] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
newRules := make([]windowrules.WindowRule, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
if r, ok := ruleMap[id]; ok {
|
||||||
|
newRules = append(newRules, r)
|
||||||
|
delete(ruleMap, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range ruleMap {
|
||||||
|
newRules = append(newRules, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.writeDMSRules(newRules)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dmsRuleCommentRegex = regexp.MustCompile(`^#\s*DMS-RULE:\s*id=([^,]+),\s*name=(.*)$`)
|
||||||
|
|
||||||
|
func (p *HyprlandWritableProvider) LoadDMSRules() ([]windowrules.WindowRule, error) {
|
||||||
|
rulesPath := p.GetOverridePath()
|
||||||
|
data, err := os.ReadFile(rulesPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return []windowrules.WindowRule{}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules []windowrules.WindowRule
|
||||||
|
var currentID, currentName string
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
|
||||||
|
if matches := dmsRuleCommentRegex.FindStringSubmatch(trimmed); matches != nil {
|
||||||
|
currentID = matches[1]
|
||||||
|
currentName = matches[2]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(trimmed, "windowrulev2") {
|
||||||
|
parser := NewHyprlandRulesParser(p.configDir)
|
||||||
|
hrule := parser.parseWindowRuleLine(trimmed)
|
||||||
|
if hrule == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
wr := windowrules.WindowRule{
|
||||||
|
ID: currentID,
|
||||||
|
Name: currentName,
|
||||||
|
Enabled: true,
|
||||||
|
Source: rulesPath,
|
||||||
|
MatchCriteria: windowrules.MatchCriteria{
|
||||||
|
AppID: hrule.MatchClass,
|
||||||
|
Title: hrule.MatchTitle,
|
||||||
|
XWayland: hrule.MatchXWayland,
|
||||||
|
IsFloating: hrule.MatchFloating,
|
||||||
|
Fullscreen: hrule.MatchFullscreen,
|
||||||
|
Pinned: hrule.MatchPinned,
|
||||||
|
Initialised: hrule.MatchInitialised,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
applyHyprlandRuleAction(&wr.Actions, hrule.Rule, hrule.Value)
|
||||||
|
|
||||||
|
if wr.ID == "" {
|
||||||
|
wr.ID = hrule.MatchClass
|
||||||
|
if wr.ID == "" {
|
||||||
|
wr.ID = hrule.MatchTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, wr)
|
||||||
|
currentID = ""
|
||||||
|
currentName = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandWritableProvider) writeDMSRules(rules []windowrules.WindowRule) error {
|
||||||
|
rulesPath := p.GetOverridePath()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(rulesPath), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
lines = append(lines, "# DMS Window Rules - Managed by DankMaterialShell")
|
||||||
|
lines = append(lines, "# Do not edit manually - changes may be overwritten")
|
||||||
|
lines = append(lines, "")
|
||||||
|
|
||||||
|
for _, rule := range rules {
|
||||||
|
lines = append(lines, p.formatRuleLines(rule)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(rulesPath, []byte(strings.Join(lines, "\n")), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandWritableProvider) formatRuleLines(rule windowrules.WindowRule) []string {
|
||||||
|
var lines []string
|
||||||
|
lines = append(lines, fmt.Sprintf("# DMS-RULE: id=%s, name=%s", rule.ID, rule.Name))
|
||||||
|
|
||||||
|
var matchParts []string
|
||||||
|
if rule.MatchCriteria.AppID != "" {
|
||||||
|
matchParts = append(matchParts, fmt.Sprintf("class:%s", rule.MatchCriteria.AppID))
|
||||||
|
}
|
||||||
|
if rule.MatchCriteria.Title != "" {
|
||||||
|
matchParts = append(matchParts, fmt.Sprintf("title:%s", rule.MatchCriteria.Title))
|
||||||
|
}
|
||||||
|
if rule.MatchCriteria.XWayland != nil {
|
||||||
|
matchParts = append(matchParts, fmt.Sprintf("xwayland:%d", boolToInt(*rule.MatchCriteria.XWayland)))
|
||||||
|
}
|
||||||
|
if rule.MatchCriteria.IsFloating != nil {
|
||||||
|
matchParts = append(matchParts, fmt.Sprintf("floating:%d", boolToInt(*rule.MatchCriteria.IsFloating)))
|
||||||
|
}
|
||||||
|
if rule.MatchCriteria.Fullscreen != nil {
|
||||||
|
matchParts = append(matchParts, fmt.Sprintf("fullscreen:%d", boolToInt(*rule.MatchCriteria.Fullscreen)))
|
||||||
|
}
|
||||||
|
if rule.MatchCriteria.Pinned != nil {
|
||||||
|
matchParts = append(matchParts, fmt.Sprintf("pinned:%d", boolToInt(*rule.MatchCriteria.Pinned)))
|
||||||
|
}
|
||||||
|
|
||||||
|
matchStr := strings.Join(matchParts, ", ")
|
||||||
|
a := rule.Actions
|
||||||
|
|
||||||
|
if a.OpenFloating != nil && *a.OpenFloating {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = float, %s", matchStr))
|
||||||
|
}
|
||||||
|
if a.Tile != nil && *a.Tile {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = tile, %s", matchStr))
|
||||||
|
}
|
||||||
|
if a.OpenFullscreen != nil && *a.OpenFullscreen {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = fullscreen, %s", matchStr))
|
||||||
|
}
|
||||||
|
if a.OpenMaximized != nil && *a.OpenMaximized {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = maximize, %s", matchStr))
|
||||||
|
}
|
||||||
|
if a.NoFocus != nil && *a.NoFocus {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = nofocus, %s", matchStr))
|
||||||
|
}
|
||||||
|
if a.NoBorder != nil && *a.NoBorder {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = noborder, %s", matchStr))
|
||||||
|
}
|
||||||
|
if a.NoShadow != nil && *a.NoShadow {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = noshadow, %s", matchStr))
|
||||||
|
}
|
||||||
|
if a.NoDim != nil && *a.NoDim {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = nodim, %s", matchStr))
|
||||||
|
}
|
||||||
|
if a.NoBlur != nil && *a.NoBlur {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = noblur, %s", matchStr))
|
||||||
|
}
|
||||||
|
if a.NoAnim != nil && *a.NoAnim {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = noanim, %s", matchStr))
|
||||||
|
}
|
||||||
|
if a.NoRounding != nil && *a.NoRounding {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = norounding, %s", matchStr))
|
||||||
|
}
|
||||||
|
if a.Pin != nil && *a.Pin {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = pin, %s", matchStr))
|
||||||
|
}
|
||||||
|
if a.Opaque != nil && *a.Opaque {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = opaque, %s", matchStr))
|
||||||
|
}
|
||||||
|
if a.ForcergbX != nil && *a.ForcergbX {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = forcergbx, %s", matchStr))
|
||||||
|
}
|
||||||
|
if a.Opacity != nil {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = opacity %.2f, %s", *a.Opacity, matchStr))
|
||||||
|
}
|
||||||
|
if a.Size != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = size %s, %s", a.Size, matchStr))
|
||||||
|
}
|
||||||
|
if a.Move != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = move %s, %s", a.Move, matchStr))
|
||||||
|
}
|
||||||
|
if a.Monitor != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = monitor %s, %s", a.Monitor, matchStr))
|
||||||
|
}
|
||||||
|
if a.Workspace != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = workspace %s, %s", a.Workspace, matchStr))
|
||||||
|
}
|
||||||
|
if a.CornerRadius != nil {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = rounding %d, %s", *a.CornerRadius, matchStr))
|
||||||
|
}
|
||||||
|
if a.Idleinhibit != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf("windowrulev2 = idleinhibit %s, %s", a.Idleinhibit, matchStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(lines) == 1 {
|
||||||
|
lines = append(lines, fmt.Sprintf("# (no actions defined for rule %s)", rule.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = append(lines, "")
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolToInt(b bool) int {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
280
core/internal/windowrules/providers/hyprland_parser_test.go
Normal file
280
core/internal/windowrules/providers/hyprland_parser_test.go
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseWindowRuleV1(t *testing.T) {
|
||||||
|
parser := NewHyprlandRulesParser("")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
line string
|
||||||
|
wantClass string
|
||||||
|
wantRule string
|
||||||
|
wantNil bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic float rule",
|
||||||
|
line: "windowrule = float, ^(firefox)$",
|
||||||
|
wantClass: "^(firefox)$",
|
||||||
|
wantRule: "float",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tile rule",
|
||||||
|
line: "windowrule = tile, steam",
|
||||||
|
wantClass: "steam",
|
||||||
|
wantRule: "tile",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no match returns empty class",
|
||||||
|
line: "windowrule = float",
|
||||||
|
wantClass: "",
|
||||||
|
wantRule: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := parser.parseWindowRuleLine(tt.line)
|
||||||
|
if tt.wantNil {
|
||||||
|
if result != nil {
|
||||||
|
t.Errorf("expected nil, got %+v", result)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("expected non-nil result")
|
||||||
|
}
|
||||||
|
if result.MatchClass != tt.wantClass {
|
||||||
|
t.Errorf("MatchClass = %q, want %q", result.MatchClass, tt.wantClass)
|
||||||
|
}
|
||||||
|
if result.Rule != tt.wantRule {
|
||||||
|
t.Errorf("Rule = %q, want %q", result.Rule, tt.wantRule)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseWindowRuleV2(t *testing.T) {
|
||||||
|
parser := NewHyprlandRulesParser("")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
line string
|
||||||
|
wantClass string
|
||||||
|
wantTitle string
|
||||||
|
wantRule string
|
||||||
|
wantValue string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "float with class",
|
||||||
|
line: "windowrulev2 = float, class:^(firefox)$",
|
||||||
|
wantClass: "^(firefox)$",
|
||||||
|
wantRule: "float",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "opacity with value",
|
||||||
|
line: "windowrulev2 = opacity 0.8, class:^(code)$",
|
||||||
|
wantClass: "^(code)$",
|
||||||
|
wantRule: "opacity",
|
||||||
|
wantValue: "0.8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "size with value and title",
|
||||||
|
line: "windowrulev2 = size 800 600, class:^(steam)$, title:Settings",
|
||||||
|
wantClass: "^(steam)$",
|
||||||
|
wantTitle: "Settings",
|
||||||
|
wantRule: "size",
|
||||||
|
wantValue: "800 600",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := parser.parseWindowRuleLine(tt.line)
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("expected non-nil result")
|
||||||
|
}
|
||||||
|
if result.MatchClass != tt.wantClass {
|
||||||
|
t.Errorf("MatchClass = %q, want %q", result.MatchClass, tt.wantClass)
|
||||||
|
}
|
||||||
|
if result.MatchTitle != tt.wantTitle {
|
||||||
|
t.Errorf("MatchTitle = %q, want %q", result.MatchTitle, tt.wantTitle)
|
||||||
|
}
|
||||||
|
if result.Rule != tt.wantRule {
|
||||||
|
t.Errorf("Rule = %q, want %q", result.Rule, tt.wantRule)
|
||||||
|
}
|
||||||
|
if result.Value != tt.wantValue {
|
||||||
|
t.Errorf("Value = %q, want %q", result.Value, tt.wantValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertHyprlandRulesToWindowRules(t *testing.T) {
|
||||||
|
hyprRules := []HyprlandWindowRule{
|
||||||
|
{MatchClass: "^(firefox)$", Rule: "float"},
|
||||||
|
{MatchClass: "^(code)$", Rule: "opacity", Value: "0.9"},
|
||||||
|
{MatchClass: "^(steam)$", Rule: "maximize"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := ConvertHyprlandRulesToWindowRules(hyprRules)
|
||||||
|
|
||||||
|
if len(result) != 3 {
|
||||||
|
t.Errorf("expected 3 rules, got %d", len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result[0].MatchCriteria.AppID != "^(firefox)$" {
|
||||||
|
t.Errorf("rule 0 AppID = %q, want ^(firefox)$", result[0].MatchCriteria.AppID)
|
||||||
|
}
|
||||||
|
if result[0].Actions.OpenFloating == nil || !*result[0].Actions.OpenFloating {
|
||||||
|
t.Error("rule 0 should have OpenFloating = true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result[1].Actions.Opacity == nil || *result[1].Actions.Opacity != 0.9 {
|
||||||
|
t.Errorf("rule 1 Opacity = %v, want 0.9", result[1].Actions.Opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result[2].Actions.OpenMaximized == nil || !*result[2].Actions.OpenMaximized {
|
||||||
|
t.Error("rule 2 should have OpenMaximized = true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandWritableProvider(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
provider := NewHyprlandWritableProvider(tmpDir)
|
||||||
|
|
||||||
|
if provider.Name() != "hyprland" {
|
||||||
|
t.Errorf("Name() = %q, want hyprland", provider.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPath := filepath.Join(tmpDir, "dms", "windowrules.conf")
|
||||||
|
if provider.GetOverridePath() != expectedPath {
|
||||||
|
t.Errorf("GetOverridePath() = %q, want %q", provider.GetOverridePath(), expectedPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandSetAndLoadDMSRules(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
provider := NewHyprlandWritableProvider(tmpDir)
|
||||||
|
|
||||||
|
rule := newTestWindowRule("test_id", "Test Rule", "^(firefox)$")
|
||||||
|
rule.Actions.OpenFloating = boolPtr(true)
|
||||||
|
|
||||||
|
if err := provider.SetRule(rule); err != nil {
|
||||||
|
t.Fatalf("SetRule failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rules, err := provider.LoadDMSRules()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadDMSRules failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rules) != 1 {
|
||||||
|
t.Fatalf("expected 1 rule, got %d", len(rules))
|
||||||
|
}
|
||||||
|
|
||||||
|
if rules[0].ID != "test_id" {
|
||||||
|
t.Errorf("ID = %q, want test_id", rules[0].ID)
|
||||||
|
}
|
||||||
|
if rules[0].MatchCriteria.AppID != "^(firefox)$" {
|
||||||
|
t.Errorf("AppID = %q, want ^(firefox)$", rules[0].MatchCriteria.AppID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandRemoveRule(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
provider := NewHyprlandWritableProvider(tmpDir)
|
||||||
|
|
||||||
|
rule1 := newTestWindowRule("rule1", "Rule 1", "^(app1)$")
|
||||||
|
rule1.Actions.OpenFloating = boolPtr(true)
|
||||||
|
rule2 := newTestWindowRule("rule2", "Rule 2", "^(app2)$")
|
||||||
|
rule2.Actions.OpenFloating = boolPtr(true)
|
||||||
|
|
||||||
|
_ = provider.SetRule(rule1)
|
||||||
|
_ = provider.SetRule(rule2)
|
||||||
|
|
||||||
|
if err := provider.RemoveRule("rule1"); err != nil {
|
||||||
|
t.Fatalf("RemoveRule failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rules, _ := provider.LoadDMSRules()
|
||||||
|
if len(rules) != 1 {
|
||||||
|
t.Fatalf("expected 1 rule after removal, got %d", len(rules))
|
||||||
|
}
|
||||||
|
if rules[0].ID != "rule2" {
|
||||||
|
t.Errorf("remaining rule ID = %q, want rule2", rules[0].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandReorderRules(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
provider := NewHyprlandWritableProvider(tmpDir)
|
||||||
|
|
||||||
|
rule1 := newTestWindowRule("rule1", "Rule 1", "^(app1)$")
|
||||||
|
rule1.Actions.OpenFloating = boolPtr(true)
|
||||||
|
rule2 := newTestWindowRule("rule2", "Rule 2", "^(app2)$")
|
||||||
|
rule2.Actions.OpenFloating = boolPtr(true)
|
||||||
|
rule3 := newTestWindowRule("rule3", "Rule 3", "^(app3)$")
|
||||||
|
rule3.Actions.OpenFloating = boolPtr(true)
|
||||||
|
|
||||||
|
_ = provider.SetRule(rule1)
|
||||||
|
_ = provider.SetRule(rule2)
|
||||||
|
_ = provider.SetRule(rule3)
|
||||||
|
|
||||||
|
if err := provider.ReorderRules([]string{"rule3", "rule1", "rule2"}); err != nil {
|
||||||
|
t.Fatalf("ReorderRules failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rules, _ := provider.LoadDMSRules()
|
||||||
|
if len(rules) != 3 {
|
||||||
|
t.Fatalf("expected 3 rules, got %d", len(rules))
|
||||||
|
}
|
||||||
|
expectedOrder := []string{"rule3", "rule1", "rule2"}
|
||||||
|
for i, expectedID := range expectedOrder {
|
||||||
|
if rules[i].ID != expectedID {
|
||||||
|
t.Errorf("rule %d ID = %q, want %q", i, rules[i].ID, expectedID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandParseConfigWithSource(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
mainConfig := `
|
||||||
|
windowrulev2 = float, class:^(mainapp)$
|
||||||
|
source = ./extra.conf
|
||||||
|
`
|
||||||
|
extraConfig := `
|
||||||
|
windowrulev2 = tile, class:^(extraapp)$
|
||||||
|
`
|
||||||
|
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.conf"), []byte(mainConfig), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "extra.conf"), []byte(extraConfig), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := NewHyprlandRulesParser(tmpDir)
|
||||||
|
rules, err := parser.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rules) != 2 {
|
||||||
|
t.Errorf("expected 2 rules, got %d", len(rules))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolToInt(t *testing.T) {
|
||||||
|
if boolToInt(true) != 1 {
|
||||||
|
t.Error("boolToInt(true) should be 1")
|
||||||
|
}
|
||||||
|
if boolToInt(false) != 0 {
|
||||||
|
t.Error("boolToInt(false) should be 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
873
core/internal/windowrules/providers/niri_parser.go
Normal file
873
core/internal/windowrules/providers/niri_parser.go
Normal file
@@ -0,0 +1,873 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sblinch/kdl-go"
|
||||||
|
"github.com/sblinch/kdl-go/document"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NiriWindowRule struct {
|
||||||
|
MatchAppID string
|
||||||
|
MatchTitle string
|
||||||
|
MatchIsFloating *bool
|
||||||
|
MatchIsActive *bool
|
||||||
|
MatchIsFocused *bool
|
||||||
|
MatchIsActiveInColumn *bool
|
||||||
|
MatchIsWindowCastTarget *bool
|
||||||
|
MatchIsUrgent *bool
|
||||||
|
MatchAtStartup *bool
|
||||||
|
Opacity *float64
|
||||||
|
OpenFloating *bool
|
||||||
|
OpenMaximized *bool
|
||||||
|
OpenMaximizedToEdges *bool
|
||||||
|
OpenFullscreen *bool
|
||||||
|
OpenFocused *bool
|
||||||
|
OpenOnOutput string
|
||||||
|
OpenOnWorkspace string
|
||||||
|
DefaultColumnWidth string
|
||||||
|
DefaultWindowHeight string
|
||||||
|
VariableRefreshRate *bool
|
||||||
|
BlockOutFrom string
|
||||||
|
DefaultColumnDisplay string
|
||||||
|
ScrollFactor *float64
|
||||||
|
CornerRadius *int
|
||||||
|
ClipToGeometry *bool
|
||||||
|
TiledState *bool
|
||||||
|
MinWidth *int
|
||||||
|
MaxWidth *int
|
||||||
|
MinHeight *int
|
||||||
|
MaxHeight *int
|
||||||
|
BorderColor string
|
||||||
|
FocusRingColor string
|
||||||
|
FocusRingOff *bool
|
||||||
|
BorderOff *bool
|
||||||
|
DrawBorderWithBg *bool
|
||||||
|
Source string
|
||||||
|
}
|
||||||
|
|
||||||
|
type NiriRulesParser struct {
|
||||||
|
configDir string
|
||||||
|
processedFiles map[string]bool
|
||||||
|
rules []NiriWindowRule
|
||||||
|
currentSource string
|
||||||
|
dmsRulesIncluded bool
|
||||||
|
dmsRulesExists bool
|
||||||
|
includeCount int
|
||||||
|
dmsIncludePos int
|
||||||
|
rulesAfterDMS int
|
||||||
|
dmsProcessed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNiriRulesParser(configDir string) *NiriRulesParser {
|
||||||
|
return &NiriRulesParser{
|
||||||
|
configDir: configDir,
|
||||||
|
processedFiles: make(map[string]bool),
|
||||||
|
rules: []NiriWindowRule{},
|
||||||
|
dmsIncludePos: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriRulesParser) Parse() ([]NiriWindowRule, error) {
|
||||||
|
dmsRulesPath := filepath.Join(p.configDir, "dms", "windowrules.kdl")
|
||||||
|
if _, err := os.Stat(dmsRulesPath); err == nil {
|
||||||
|
p.dmsRulesExists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(p.configDir, "config.kdl")
|
||||||
|
if err := p.parseFile(configPath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.dmsRulesExists && !p.dmsProcessed {
|
||||||
|
p.parseDMSRulesDirectly(dmsRulesPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.rules, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriRulesParser) parseDMSRulesDirectly(dmsRulesPath string) {
|
||||||
|
data, err := os.ReadFile(dmsRulesPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSource := p.currentSource
|
||||||
|
p.currentSource = dmsRulesPath
|
||||||
|
p.processNodes(doc.Nodes, filepath.Dir(dmsRulesPath))
|
||||||
|
p.currentSource = prevSource
|
||||||
|
p.dmsProcessed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriRulesParser) parseFile(filePath string) error {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.processedFiles[absPath] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p.processedFiles[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSource := p.currentSource
|
||||||
|
p.currentSource = absPath
|
||||||
|
baseDir := filepath.Dir(absPath)
|
||||||
|
p.processNodes(doc.Nodes, baseDir)
|
||||||
|
p.currentSource = prevSource
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriRulesParser) processNodes(nodes []*document.Node, baseDir string) {
|
||||||
|
for _, node := range nodes {
|
||||||
|
name := node.Name.String()
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case "include":
|
||||||
|
p.handleInclude(node, baseDir)
|
||||||
|
case "window-rule":
|
||||||
|
p.parseWindowRuleNode(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriRulesParser) handleInclude(node *document.Node, baseDir string) {
|
||||||
|
if len(node.Arguments) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
includePath := strings.Trim(node.Arguments[0].String(), "\"")
|
||||||
|
isDMSInclude := includePath == "dms/windowrules.kdl" || strings.HasSuffix(includePath, "/dms/windowrules.kdl")
|
||||||
|
|
||||||
|
p.includeCount++
|
||||||
|
if isDMSInclude {
|
||||||
|
p.dmsRulesIncluded = true
|
||||||
|
p.dmsIncludePos = p.includeCount
|
||||||
|
p.dmsProcessed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := filepath.Join(baseDir, includePath)
|
||||||
|
if filepath.IsAbs(includePath) {
|
||||||
|
fullPath = includePath
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = p.parseFile(fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriRulesParser) parseWindowRuleNode(node *document.Node) {
|
||||||
|
if node.Children == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rule := NiriWindowRule{
|
||||||
|
Source: p.currentSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range node.Children {
|
||||||
|
childName := child.Name.String()
|
||||||
|
|
||||||
|
switch childName {
|
||||||
|
case "match":
|
||||||
|
p.parseMatchNode(child, &rule)
|
||||||
|
case "opacity":
|
||||||
|
if len(child.Arguments) > 0 {
|
||||||
|
val := child.Arguments[0].ResolvedValue()
|
||||||
|
if f, ok := val.(float64); ok {
|
||||||
|
rule.Opacity = &f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "open-floating":
|
||||||
|
b := p.parseBoolArg(child)
|
||||||
|
rule.OpenFloating = &b
|
||||||
|
case "open-maximized":
|
||||||
|
b := p.parseBoolArg(child)
|
||||||
|
rule.OpenMaximized = &b
|
||||||
|
case "open-maximized-to-edges":
|
||||||
|
b := p.parseBoolArg(child)
|
||||||
|
rule.OpenMaximizedToEdges = &b
|
||||||
|
case "open-fullscreen":
|
||||||
|
b := p.parseBoolArg(child)
|
||||||
|
rule.OpenFullscreen = &b
|
||||||
|
case "open-focused":
|
||||||
|
b := p.parseBoolArg(child)
|
||||||
|
rule.OpenFocused = &b
|
||||||
|
case "open-on-output":
|
||||||
|
if len(child.Arguments) > 0 {
|
||||||
|
rule.OpenOnOutput = child.Arguments[0].ValueString()
|
||||||
|
}
|
||||||
|
case "open-on-workspace":
|
||||||
|
if len(child.Arguments) > 0 {
|
||||||
|
rule.OpenOnWorkspace = child.Arguments[0].ValueString()
|
||||||
|
}
|
||||||
|
case "default-column-width":
|
||||||
|
rule.DefaultColumnWidth = p.parseSizeNode(child)
|
||||||
|
case "default-window-height":
|
||||||
|
rule.DefaultWindowHeight = p.parseSizeNode(child)
|
||||||
|
case "variable-refresh-rate":
|
||||||
|
b := p.parseBoolArg(child)
|
||||||
|
rule.VariableRefreshRate = &b
|
||||||
|
case "block-out-from":
|
||||||
|
if len(child.Arguments) > 0 {
|
||||||
|
rule.BlockOutFrom = child.Arguments[0].ValueString()
|
||||||
|
}
|
||||||
|
case "default-column-display":
|
||||||
|
if len(child.Arguments) > 0 {
|
||||||
|
rule.DefaultColumnDisplay = child.Arguments[0].ValueString()
|
||||||
|
}
|
||||||
|
case "scroll-factor":
|
||||||
|
if len(child.Arguments) > 0 {
|
||||||
|
val := child.Arguments[0].ResolvedValue()
|
||||||
|
if f, ok := val.(float64); ok {
|
||||||
|
rule.ScrollFactor = &f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "geometry-corner-radius":
|
||||||
|
if len(child.Arguments) > 0 {
|
||||||
|
val := child.Arguments[0].ResolvedValue()
|
||||||
|
if i, ok := val.(int64); ok {
|
||||||
|
intVal := int(i)
|
||||||
|
rule.CornerRadius = &intVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "clip-to-geometry":
|
||||||
|
b := p.parseBoolArg(child)
|
||||||
|
rule.ClipToGeometry = &b
|
||||||
|
case "tiled-state":
|
||||||
|
b := p.parseBoolArg(child)
|
||||||
|
rule.TiledState = &b
|
||||||
|
case "min-width":
|
||||||
|
if len(child.Arguments) > 0 {
|
||||||
|
val := child.Arguments[0].ResolvedValue()
|
||||||
|
if i, ok := val.(int64); ok {
|
||||||
|
intVal := int(i)
|
||||||
|
rule.MinWidth = &intVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "max-width":
|
||||||
|
if len(child.Arguments) > 0 {
|
||||||
|
val := child.Arguments[0].ResolvedValue()
|
||||||
|
if i, ok := val.(int64); ok {
|
||||||
|
intVal := int(i)
|
||||||
|
rule.MaxWidth = &intVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "min-height":
|
||||||
|
if len(child.Arguments) > 0 {
|
||||||
|
val := child.Arguments[0].ResolvedValue()
|
||||||
|
if i, ok := val.(int64); ok {
|
||||||
|
intVal := int(i)
|
||||||
|
rule.MinHeight = &intVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "max-height":
|
||||||
|
if len(child.Arguments) > 0 {
|
||||||
|
val := child.Arguments[0].ResolvedValue()
|
||||||
|
if i, ok := val.(int64); ok {
|
||||||
|
intVal := int(i)
|
||||||
|
rule.MaxHeight = &intVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "border":
|
||||||
|
p.parseBorderNode(child, &rule)
|
||||||
|
case "focus-ring":
|
||||||
|
p.parseFocusRingNode(child, &rule)
|
||||||
|
case "draw-border-with-background":
|
||||||
|
b := p.parseBoolArg(child)
|
||||||
|
rule.DrawBorderWithBg = &b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.rules = append(p.rules, rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriRulesParser) parseSizeNode(node *document.Node) string {
|
||||||
|
if node.Children == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, child := range node.Children {
|
||||||
|
name := child.Name.String()
|
||||||
|
if len(child.Arguments) > 0 {
|
||||||
|
val := child.Arguments[0].ResolvedValue()
|
||||||
|
switch name {
|
||||||
|
case "fixed":
|
||||||
|
if i, ok := val.(int64); ok {
|
||||||
|
return "fixed " + strconv.FormatInt(i, 10)
|
||||||
|
}
|
||||||
|
case "proportion":
|
||||||
|
if f, ok := val.(float64); ok {
|
||||||
|
return "proportion " + strconv.FormatFloat(f, 'f', -1, 64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriRulesParser) parseMatchNode(node *document.Node, rule *NiriWindowRule) {
|
||||||
|
if node.Properties == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := node.Properties.Get("app-id"); ok {
|
||||||
|
rule.MatchAppID = val.ValueString()
|
||||||
|
}
|
||||||
|
if val, ok := node.Properties.Get("title"); ok {
|
||||||
|
rule.MatchTitle = val.ValueString()
|
||||||
|
}
|
||||||
|
if val, ok := node.Properties.Get("is-floating"); ok {
|
||||||
|
b := val.ValueString() == "true"
|
||||||
|
rule.MatchIsFloating = &b
|
||||||
|
}
|
||||||
|
if val, ok := node.Properties.Get("is-active"); ok {
|
||||||
|
b := val.ValueString() == "true"
|
||||||
|
rule.MatchIsActive = &b
|
||||||
|
}
|
||||||
|
if val, ok := node.Properties.Get("is-focused"); ok {
|
||||||
|
b := val.ValueString() == "true"
|
||||||
|
rule.MatchIsFocused = &b
|
||||||
|
}
|
||||||
|
if val, ok := node.Properties.Get("is-active-in-column"); ok {
|
||||||
|
b := val.ValueString() == "true"
|
||||||
|
rule.MatchIsActiveInColumn = &b
|
||||||
|
}
|
||||||
|
if val, ok := node.Properties.Get("is-window-cast-target"); ok {
|
||||||
|
b := val.ValueString() == "true"
|
||||||
|
rule.MatchIsWindowCastTarget = &b
|
||||||
|
}
|
||||||
|
if val, ok := node.Properties.Get("is-urgent"); ok {
|
||||||
|
b := val.ValueString() == "true"
|
||||||
|
rule.MatchIsUrgent = &b
|
||||||
|
}
|
||||||
|
if val, ok := node.Properties.Get("at-startup"); ok {
|
||||||
|
b := val.ValueString() == "true"
|
||||||
|
rule.MatchAtStartup = &b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriRulesParser) parseBorderNode(node *document.Node, rule *NiriWindowRule) {
|
||||||
|
if node.Children == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range node.Children {
|
||||||
|
switch child.Name.String() {
|
||||||
|
case "off":
|
||||||
|
b := true
|
||||||
|
rule.BorderOff = &b
|
||||||
|
case "active-color":
|
||||||
|
if len(child.Arguments) > 0 {
|
||||||
|
rule.BorderColor = child.Arguments[0].ValueString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriRulesParser) parseFocusRingNode(node *document.Node, rule *NiriWindowRule) {
|
||||||
|
if node.Children == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range node.Children {
|
||||||
|
switch child.Name.String() {
|
||||||
|
case "off":
|
||||||
|
b := true
|
||||||
|
rule.FocusRingOff = &b
|
||||||
|
case "active-color":
|
||||||
|
if len(child.Arguments) > 0 {
|
||||||
|
rule.FocusRingColor = child.Arguments[0].ValueString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriRulesParser) parseBoolArg(node *document.Node) bool {
|
||||||
|
if len(node.Arguments) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return node.Arguments[0].ValueString() != "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriRulesParser) HasDMSRulesIncluded() bool {
|
||||||
|
return p.dmsRulesIncluded
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriRulesParser) buildDMSStatus() *windowrules.DMSRulesStatus {
|
||||||
|
status := &windowrules.DMSRulesStatus{
|
||||||
|
Exists: p.dmsRulesExists,
|
||||||
|
Included: p.dmsRulesIncluded,
|
||||||
|
IncludePosition: p.dmsIncludePos,
|
||||||
|
TotalIncludes: p.includeCount,
|
||||||
|
RulesAfterDMS: p.rulesAfterDMS,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case !p.dmsRulesExists:
|
||||||
|
status.Effective = false
|
||||||
|
status.StatusMessage = "dms/windowrules.kdl does not exist"
|
||||||
|
case !p.dmsRulesIncluded:
|
||||||
|
status.Effective = false
|
||||||
|
status.StatusMessage = "dms/windowrules.kdl is not included in config.kdl"
|
||||||
|
case p.rulesAfterDMS > 0:
|
||||||
|
status.Effective = true
|
||||||
|
status.OverriddenBy = p.rulesAfterDMS
|
||||||
|
status.StatusMessage = "Some DMS rules may be overridden by config rules"
|
||||||
|
default:
|
||||||
|
status.Effective = true
|
||||||
|
status.StatusMessage = "DMS window rules are active"
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
type NiriRulesParseResult struct {
|
||||||
|
Rules []NiriWindowRule
|
||||||
|
DMSRulesIncluded bool
|
||||||
|
DMSStatus *windowrules.DMSRulesStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseNiriWindowRules(configDir string) (*NiriRulesParseResult, error) {
|
||||||
|
parser := NewNiriRulesParser(configDir)
|
||||||
|
rules, err := parser.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &NiriRulesParseResult{
|
||||||
|
Rules: rules,
|
||||||
|
DMSRulesIncluded: parser.HasDMSRulesIncluded(),
|
||||||
|
DMSStatus: parser.buildDMSStatus(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertNiriRulesToWindowRules(niriRules []NiriWindowRule) []windowrules.WindowRule {
|
||||||
|
result := make([]windowrules.WindowRule, 0, len(niriRules))
|
||||||
|
for i, nr := range niriRules {
|
||||||
|
wr := windowrules.WindowRule{
|
||||||
|
ID: fmt.Sprintf("rule_%d", i),
|
||||||
|
Enabled: true,
|
||||||
|
Source: nr.Source,
|
||||||
|
MatchCriteria: windowrules.MatchCriteria{
|
||||||
|
AppID: nr.MatchAppID,
|
||||||
|
Title: nr.MatchTitle,
|
||||||
|
IsFloating: nr.MatchIsFloating,
|
||||||
|
IsActive: nr.MatchIsActive,
|
||||||
|
IsFocused: nr.MatchIsFocused,
|
||||||
|
IsActiveInColumn: nr.MatchIsActiveInColumn,
|
||||||
|
IsWindowCastTarget: nr.MatchIsWindowCastTarget,
|
||||||
|
IsUrgent: nr.MatchIsUrgent,
|
||||||
|
AtStartup: nr.MatchAtStartup,
|
||||||
|
},
|
||||||
|
Actions: windowrules.Actions{
|
||||||
|
Opacity: nr.Opacity,
|
||||||
|
OpenFloating: nr.OpenFloating,
|
||||||
|
OpenMaximized: nr.OpenMaximized,
|
||||||
|
OpenMaximizedToEdges: nr.OpenMaximizedToEdges,
|
||||||
|
OpenFullscreen: nr.OpenFullscreen,
|
||||||
|
OpenFocused: nr.OpenFocused,
|
||||||
|
OpenOnOutput: nr.OpenOnOutput,
|
||||||
|
OpenOnWorkspace: nr.OpenOnWorkspace,
|
||||||
|
DefaultColumnWidth: nr.DefaultColumnWidth,
|
||||||
|
DefaultWindowHeight: nr.DefaultWindowHeight,
|
||||||
|
VariableRefreshRate: nr.VariableRefreshRate,
|
||||||
|
BlockOutFrom: nr.BlockOutFrom,
|
||||||
|
DefaultColumnDisplay: nr.DefaultColumnDisplay,
|
||||||
|
ScrollFactor: nr.ScrollFactor,
|
||||||
|
CornerRadius: nr.CornerRadius,
|
||||||
|
ClipToGeometry: nr.ClipToGeometry,
|
||||||
|
TiledState: nr.TiledState,
|
||||||
|
MinWidth: nr.MinWidth,
|
||||||
|
MaxWidth: nr.MaxWidth,
|
||||||
|
MinHeight: nr.MinHeight,
|
||||||
|
MaxHeight: nr.MaxHeight,
|
||||||
|
BorderColor: nr.BorderColor,
|
||||||
|
FocusRingColor: nr.FocusRingColor,
|
||||||
|
FocusRingOff: nr.FocusRingOff,
|
||||||
|
BorderOff: nr.BorderOff,
|
||||||
|
DrawBorderWithBg: nr.DrawBorderWithBg,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result = append(result, wr)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
type NiriWritableProvider struct {
|
||||||
|
configDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNiriWritableProvider(configDir string) *NiriWritableProvider {
|
||||||
|
return &NiriWritableProvider{configDir: configDir}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriWritableProvider) Name() string {
|
||||||
|
return "niri"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriWritableProvider) GetOverridePath() string {
|
||||||
|
return filepath.Join(p.configDir, "dms", "windowrules.kdl")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriWritableProvider) GetRuleSet() (*windowrules.RuleSet, error) {
|
||||||
|
result, err := ParseNiriWindowRules(p.configDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &windowrules.RuleSet{
|
||||||
|
Title: "Niri Window Rules",
|
||||||
|
Provider: "niri",
|
||||||
|
Rules: ConvertNiriRulesToWindowRules(result.Rules),
|
||||||
|
DMSRulesIncluded: result.DMSRulesIncluded,
|
||||||
|
DMSStatus: result.DMSStatus,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriWritableProvider) SetRule(rule windowrules.WindowRule) error {
|
||||||
|
rules, err := p.LoadDMSRules()
|
||||||
|
if err != nil {
|
||||||
|
rules = []windowrules.WindowRule{}
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for i, r := range rules {
|
||||||
|
if r.ID == rule.ID {
|
||||||
|
rules[i] = rule
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
rules = append(rules, rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.writeDMSRules(rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriWritableProvider) RemoveRule(id string) error {
|
||||||
|
rules, err := p.LoadDMSRules()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newRules := make([]windowrules.WindowRule, 0, len(rules))
|
||||||
|
for _, r := range rules {
|
||||||
|
if r.ID != id {
|
||||||
|
newRules = append(newRules, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.writeDMSRules(newRules)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriWritableProvider) ReorderRules(ids []string) error {
|
||||||
|
rules, err := p.LoadDMSRules()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleMap := make(map[string]windowrules.WindowRule)
|
||||||
|
for _, r := range rules {
|
||||||
|
ruleMap[r.ID] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
newRules := make([]windowrules.WindowRule, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
if r, ok := ruleMap[id]; ok {
|
||||||
|
newRules = append(newRules, r)
|
||||||
|
delete(ruleMap, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range ruleMap {
|
||||||
|
newRules = append(newRules, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.writeDMSRules(newRules)
|
||||||
|
}
|
||||||
|
|
||||||
|
var niriMetaCommentRegex = regexp.MustCompile(`^//\s*@id=(\S*)\s*@name=(.*)$`)
|
||||||
|
|
||||||
|
func (p *NiriWritableProvider) LoadDMSRules() ([]windowrules.WindowRule, error) {
|
||||||
|
rulesPath := p.GetOverridePath()
|
||||||
|
data, err := os.ReadFile(rulesPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return []windowrules.WindowRule{}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
content := string(data)
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
|
||||||
|
type ruleMeta struct {
|
||||||
|
id string
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
var metas []ruleMeta
|
||||||
|
var currentID, currentName string
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if matches := niriMetaCommentRegex.FindStringSubmatch(trimmed); matches != nil {
|
||||||
|
currentID = matches[1]
|
||||||
|
currentName = strings.TrimSpace(matches[2])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(trimmed, "window-rule") {
|
||||||
|
metas = append(metas, ruleMeta{id: currentID, name: currentName})
|
||||||
|
currentID = ""
|
||||||
|
currentName = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, err := kdl.Parse(strings.NewReader(content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := NewNiriRulesParser(p.configDir)
|
||||||
|
parser.currentSource = rulesPath
|
||||||
|
|
||||||
|
for _, node := range doc.Nodes {
|
||||||
|
if node.Name.String() == "window-rule" {
|
||||||
|
parser.parseWindowRuleNode(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules []windowrules.WindowRule
|
||||||
|
for i, nr := range parser.rules {
|
||||||
|
id := ""
|
||||||
|
name := ""
|
||||||
|
if i < len(metas) {
|
||||||
|
id = metas[i].id
|
||||||
|
name = metas[i].name
|
||||||
|
}
|
||||||
|
if id == "" {
|
||||||
|
id = fmt.Sprintf("dms_rule_%d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wr := windowrules.WindowRule{
|
||||||
|
ID: id,
|
||||||
|
Name: name,
|
||||||
|
Enabled: true,
|
||||||
|
Source: rulesPath,
|
||||||
|
MatchCriteria: windowrules.MatchCriteria{
|
||||||
|
AppID: nr.MatchAppID,
|
||||||
|
Title: nr.MatchTitle,
|
||||||
|
IsFloating: nr.MatchIsFloating,
|
||||||
|
IsActive: nr.MatchIsActive,
|
||||||
|
IsFocused: nr.MatchIsFocused,
|
||||||
|
IsActiveInColumn: nr.MatchIsActiveInColumn,
|
||||||
|
IsWindowCastTarget: nr.MatchIsWindowCastTarget,
|
||||||
|
IsUrgent: nr.MatchIsUrgent,
|
||||||
|
AtStartup: nr.MatchAtStartup,
|
||||||
|
},
|
||||||
|
Actions: windowrules.Actions{
|
||||||
|
Opacity: nr.Opacity,
|
||||||
|
OpenFloating: nr.OpenFloating,
|
||||||
|
OpenMaximized: nr.OpenMaximized,
|
||||||
|
OpenMaximizedToEdges: nr.OpenMaximizedToEdges,
|
||||||
|
OpenFullscreen: nr.OpenFullscreen,
|
||||||
|
OpenFocused: nr.OpenFocused,
|
||||||
|
OpenOnOutput: nr.OpenOnOutput,
|
||||||
|
OpenOnWorkspace: nr.OpenOnWorkspace,
|
||||||
|
DefaultColumnWidth: nr.DefaultColumnWidth,
|
||||||
|
DefaultWindowHeight: nr.DefaultWindowHeight,
|
||||||
|
VariableRefreshRate: nr.VariableRefreshRate,
|
||||||
|
BlockOutFrom: nr.BlockOutFrom,
|
||||||
|
DefaultColumnDisplay: nr.DefaultColumnDisplay,
|
||||||
|
ScrollFactor: nr.ScrollFactor,
|
||||||
|
CornerRadius: nr.CornerRadius,
|
||||||
|
ClipToGeometry: nr.ClipToGeometry,
|
||||||
|
TiledState: nr.TiledState,
|
||||||
|
MinWidth: nr.MinWidth,
|
||||||
|
MaxWidth: nr.MaxWidth,
|
||||||
|
MinHeight: nr.MinHeight,
|
||||||
|
MaxHeight: nr.MaxHeight,
|
||||||
|
BorderColor: nr.BorderColor,
|
||||||
|
FocusRingColor: nr.FocusRingColor,
|
||||||
|
FocusRingOff: nr.FocusRingOff,
|
||||||
|
BorderOff: nr.BorderOff,
|
||||||
|
DrawBorderWithBg: nr.DrawBorderWithBg,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, wr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriWritableProvider) writeDMSRules(rules []windowrules.WindowRule) error {
|
||||||
|
rulesPath := p.GetOverridePath()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(rulesPath), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
lines = append(lines, "// DMS Window Rules - Managed by DankMaterialShell")
|
||||||
|
lines = append(lines, "// Do not edit manually - changes may be overwritten")
|
||||||
|
lines = append(lines, "")
|
||||||
|
|
||||||
|
for _, rule := range rules {
|
||||||
|
lines = append(lines, p.formatRule(rule))
|
||||||
|
lines = append(lines, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(rulesPath, []byte(strings.Join(lines, "\n")), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
|
||||||
|
var lines []string
|
||||||
|
lines = append(lines, fmt.Sprintf("// @id=%s @name=%s", rule.ID, rule.Name))
|
||||||
|
lines = append(lines, "window-rule {")
|
||||||
|
|
||||||
|
m := rule.MatchCriteria
|
||||||
|
if m.AppID != "" || m.Title != "" || m.IsFloating != nil || m.IsActive != nil ||
|
||||||
|
m.IsFocused != nil || m.IsActiveInColumn != nil || m.IsWindowCastTarget != nil ||
|
||||||
|
m.IsUrgent != nil || m.AtStartup != nil {
|
||||||
|
var matchProps []string
|
||||||
|
if m.AppID != "" {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("app-id=%q", m.AppID))
|
||||||
|
}
|
||||||
|
if m.Title != "" {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("title=%q", m.Title))
|
||||||
|
}
|
||||||
|
if m.IsFloating != nil {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("is-floating=%t", *m.IsFloating))
|
||||||
|
}
|
||||||
|
if m.IsActive != nil {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("is-active=%t", *m.IsActive))
|
||||||
|
}
|
||||||
|
if m.IsFocused != nil {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("is-focused=%t", *m.IsFocused))
|
||||||
|
}
|
||||||
|
if m.IsActiveInColumn != nil {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("is-active-in-column=%t", *m.IsActiveInColumn))
|
||||||
|
}
|
||||||
|
if m.IsWindowCastTarget != nil {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("is-window-cast-target=%t", *m.IsWindowCastTarget))
|
||||||
|
}
|
||||||
|
if m.IsUrgent != nil {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("is-urgent=%t", *m.IsUrgent))
|
||||||
|
}
|
||||||
|
if m.AtStartup != nil {
|
||||||
|
matchProps = append(matchProps, fmt.Sprintf("at-startup=%t", *m.AtStartup))
|
||||||
|
}
|
||||||
|
lines = append(lines, " match "+strings.Join(matchProps, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
a := rule.Actions
|
||||||
|
if a.Opacity != nil {
|
||||||
|
lines = append(lines, fmt.Sprintf(" opacity %.2f", *a.Opacity))
|
||||||
|
}
|
||||||
|
if a.OpenFloating != nil && *a.OpenFloating {
|
||||||
|
lines = append(lines, " open-floating true")
|
||||||
|
}
|
||||||
|
if a.OpenMaximized != nil && *a.OpenMaximized {
|
||||||
|
lines = append(lines, " open-maximized true")
|
||||||
|
}
|
||||||
|
if a.OpenMaximizedToEdges != nil && *a.OpenMaximizedToEdges {
|
||||||
|
lines = append(lines, " open-maximized-to-edges true")
|
||||||
|
}
|
||||||
|
if a.OpenFullscreen != nil && *a.OpenFullscreen {
|
||||||
|
lines = append(lines, " open-fullscreen true")
|
||||||
|
}
|
||||||
|
if a.OpenFocused != nil {
|
||||||
|
lines = append(lines, fmt.Sprintf(" open-focused %t", *a.OpenFocused))
|
||||||
|
}
|
||||||
|
if a.OpenOnOutput != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf(" open-on-output %q", a.OpenOnOutput))
|
||||||
|
}
|
||||||
|
if a.OpenOnWorkspace != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf(" open-on-workspace %q", a.OpenOnWorkspace))
|
||||||
|
}
|
||||||
|
if a.DefaultColumnWidth != "" {
|
||||||
|
lines = append(lines, formatSizeProperty("default-column-width", a.DefaultColumnWidth))
|
||||||
|
}
|
||||||
|
if a.DefaultWindowHeight != "" {
|
||||||
|
lines = append(lines, formatSizeProperty("default-window-height", a.DefaultWindowHeight))
|
||||||
|
}
|
||||||
|
if a.VariableRefreshRate != nil && *a.VariableRefreshRate {
|
||||||
|
lines = append(lines, " variable-refresh-rate true")
|
||||||
|
}
|
||||||
|
if a.BlockOutFrom != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf(" block-out-from %q", a.BlockOutFrom))
|
||||||
|
}
|
||||||
|
if a.DefaultColumnDisplay != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf(" default-column-display %q", a.DefaultColumnDisplay))
|
||||||
|
}
|
||||||
|
if a.ScrollFactor != nil {
|
||||||
|
lines = append(lines, fmt.Sprintf(" scroll-factor %.2f", *a.ScrollFactor))
|
||||||
|
}
|
||||||
|
if a.CornerRadius != nil {
|
||||||
|
lines = append(lines, fmt.Sprintf(" geometry-corner-radius %d", *a.CornerRadius))
|
||||||
|
}
|
||||||
|
if a.ClipToGeometry != nil && *a.ClipToGeometry {
|
||||||
|
lines = append(lines, " clip-to-geometry true")
|
||||||
|
}
|
||||||
|
if a.TiledState != nil && *a.TiledState {
|
||||||
|
lines = append(lines, " tiled-state true")
|
||||||
|
}
|
||||||
|
if a.MinWidth != nil {
|
||||||
|
lines = append(lines, fmt.Sprintf(" min-width %d", *a.MinWidth))
|
||||||
|
}
|
||||||
|
if a.MaxWidth != nil {
|
||||||
|
lines = append(lines, fmt.Sprintf(" max-width %d", *a.MaxWidth))
|
||||||
|
}
|
||||||
|
if a.MinHeight != nil {
|
||||||
|
lines = append(lines, fmt.Sprintf(" min-height %d", *a.MinHeight))
|
||||||
|
}
|
||||||
|
if a.MaxHeight != nil {
|
||||||
|
lines = append(lines, fmt.Sprintf(" max-height %d", *a.MaxHeight))
|
||||||
|
}
|
||||||
|
if a.BorderOff != nil && *a.BorderOff {
|
||||||
|
lines = append(lines, " border { off; }")
|
||||||
|
} else if a.BorderColor != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf(" border { active-color %q; }", a.BorderColor))
|
||||||
|
}
|
||||||
|
if a.FocusRingOff != nil && *a.FocusRingOff {
|
||||||
|
lines = append(lines, " focus-ring { off; }")
|
||||||
|
} else if a.FocusRingColor != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf(" focus-ring { active-color %q; }", a.FocusRingColor))
|
||||||
|
}
|
||||||
|
if a.DrawBorderWithBg != nil {
|
||||||
|
lines = append(lines, fmt.Sprintf(" draw-border-with-background %t", *a.DrawBorderWithBg))
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = append(lines, "}")
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatSizeProperty(name, value string) string {
|
||||||
|
parts := strings.SplitN(value, " ", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return fmt.Sprintf(" %s { }", name)
|
||||||
|
}
|
||||||
|
sizeType := parts[0]
|
||||||
|
sizeValue := parts[1]
|
||||||
|
return fmt.Sprintf(" %s { %s %s; }", name, sizeType, sizeValue)
|
||||||
|
}
|
||||||
335
core/internal/windowrules/providers/niri_parser_test.go
Normal file
335
core/internal/windowrules/providers/niri_parser_test.go
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNiriParseBasicWindowRule(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
config := `
|
||||||
|
window-rule {
|
||||||
|
match app-id="^firefox$"
|
||||||
|
opacity 0.9
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := NewNiriRulesParser(tmpDir)
|
||||||
|
rules, err := parser.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rules) != 1 {
|
||||||
|
t.Fatalf("expected 1 rule, got %d", len(rules))
|
||||||
|
}
|
||||||
|
|
||||||
|
rule := rules[0]
|
||||||
|
if rule.MatchAppID != "^firefox$" {
|
||||||
|
t.Errorf("MatchAppID = %q, want ^firefox$", rule.MatchAppID)
|
||||||
|
}
|
||||||
|
if rule.Opacity == nil || *rule.Opacity != 0.9 {
|
||||||
|
t.Errorf("Opacity = %v, want 0.9", rule.Opacity)
|
||||||
|
}
|
||||||
|
if rule.OpenFloating == nil || !*rule.OpenFloating {
|
||||||
|
t.Error("OpenFloating should be true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseMultipleRules(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
config := `
|
||||||
|
window-rule {
|
||||||
|
match app-id="app1"
|
||||||
|
open-maximized true
|
||||||
|
}
|
||||||
|
|
||||||
|
window-rule {
|
||||||
|
match app-id="app2"
|
||||||
|
open-fullscreen true
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := NewNiriRulesParser(tmpDir)
|
||||||
|
rules, err := parser.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rules) != 2 {
|
||||||
|
t.Fatalf("expected 2 rules, got %d", len(rules))
|
||||||
|
}
|
||||||
|
|
||||||
|
if rules[0].MatchAppID != "app1" {
|
||||||
|
t.Errorf("rule 0 MatchAppID = %q, want app1", rules[0].MatchAppID)
|
||||||
|
}
|
||||||
|
if rules[1].MatchAppID != "app2" {
|
||||||
|
t.Errorf("rule 1 MatchAppID = %q, want app2", rules[1].MatchAppID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertNiriRulesToWindowRules(t *testing.T) {
|
||||||
|
niriRules := []NiriWindowRule{
|
||||||
|
{MatchAppID: "^firefox$", Opacity: floatPtr(0.8)},
|
||||||
|
{MatchAppID: "^code$", OpenFloating: boolPtr(true)},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := ConvertNiriRulesToWindowRules(niriRules)
|
||||||
|
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Errorf("expected 2 rules, got %d", len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result[0].MatchCriteria.AppID != "^firefox$" {
|
||||||
|
t.Errorf("rule 0 AppID = %q, want ^firefox$", result[0].MatchCriteria.AppID)
|
||||||
|
}
|
||||||
|
if result[0].Actions.Opacity == nil || *result[0].Actions.Opacity != 0.8 {
|
||||||
|
t.Errorf("rule 0 Opacity = %v, want 0.8", result[0].Actions.Opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result[1].Actions.OpenFloating == nil || !*result[1].Actions.OpenFloating {
|
||||||
|
t.Error("rule 1 should have OpenFloating = true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriWritableProvider(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
provider := NewNiriWritableProvider(tmpDir)
|
||||||
|
|
||||||
|
if provider.Name() != "niri" {
|
||||||
|
t.Errorf("Name() = %q, want niri", provider.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPath := filepath.Join(tmpDir, "dms", "windowrules.kdl")
|
||||||
|
if provider.GetOverridePath() != expectedPath {
|
||||||
|
t.Errorf("GetOverridePath() = %q, want %q", provider.GetOverridePath(), expectedPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriSetAndLoadDMSRules(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
provider := NewNiriWritableProvider(tmpDir)
|
||||||
|
|
||||||
|
rule := newTestWindowRule("test_id", "Test Rule", "^firefox$")
|
||||||
|
rule.Actions.OpenFloating = boolPtr(true)
|
||||||
|
rule.Actions.Opacity = floatPtr(0.85)
|
||||||
|
|
||||||
|
if err := provider.SetRule(rule); err != nil {
|
||||||
|
t.Fatalf("SetRule failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rules, err := provider.LoadDMSRules()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadDMSRules failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rules) != 1 {
|
||||||
|
t.Fatalf("expected 1 rule, got %d", len(rules))
|
||||||
|
}
|
||||||
|
|
||||||
|
if rules[0].ID != "test_id" {
|
||||||
|
t.Errorf("ID = %q, want test_id", rules[0].ID)
|
||||||
|
}
|
||||||
|
if rules[0].MatchCriteria.AppID != "^firefox$" {
|
||||||
|
t.Errorf("AppID = %q, want ^firefox$", rules[0].MatchCriteria.AppID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriRemoveRule(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
provider := NewNiriWritableProvider(tmpDir)
|
||||||
|
|
||||||
|
rule1 := newTestWindowRule("rule1", "Rule 1", "app1")
|
||||||
|
rule1.Actions.OpenFloating = boolPtr(true)
|
||||||
|
rule2 := newTestWindowRule("rule2", "Rule 2", "app2")
|
||||||
|
rule2.Actions.OpenFloating = boolPtr(true)
|
||||||
|
|
||||||
|
_ = provider.SetRule(rule1)
|
||||||
|
_ = provider.SetRule(rule2)
|
||||||
|
|
||||||
|
if err := provider.RemoveRule("rule1"); err != nil {
|
||||||
|
t.Fatalf("RemoveRule failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rules, _ := provider.LoadDMSRules()
|
||||||
|
if len(rules) != 1 {
|
||||||
|
t.Fatalf("expected 1 rule after removal, got %d", len(rules))
|
||||||
|
}
|
||||||
|
if rules[0].ID != "rule2" {
|
||||||
|
t.Errorf("remaining rule ID = %q, want rule2", rules[0].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriReorderRules(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
provider := NewNiriWritableProvider(tmpDir)
|
||||||
|
|
||||||
|
rule1 := newTestWindowRule("rule1", "Rule 1", "app1")
|
||||||
|
rule1.Actions.OpenFloating = boolPtr(true)
|
||||||
|
rule2 := newTestWindowRule("rule2", "Rule 2", "app2")
|
||||||
|
rule2.Actions.OpenFloating = boolPtr(true)
|
||||||
|
rule3 := newTestWindowRule("rule3", "Rule 3", "app3")
|
||||||
|
rule3.Actions.OpenFloating = boolPtr(true)
|
||||||
|
|
||||||
|
_ = provider.SetRule(rule1)
|
||||||
|
_ = provider.SetRule(rule2)
|
||||||
|
_ = provider.SetRule(rule3)
|
||||||
|
|
||||||
|
if err := provider.ReorderRules([]string{"rule3", "rule1", "rule2"}); err != nil {
|
||||||
|
t.Fatalf("ReorderRules failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rules, _ := provider.LoadDMSRules()
|
||||||
|
if len(rules) != 3 {
|
||||||
|
t.Fatalf("expected 3 rules, got %d", len(rules))
|
||||||
|
}
|
||||||
|
expectedOrder := []string{"rule3", "rule1", "rule2"}
|
||||||
|
for i, expectedID := range expectedOrder {
|
||||||
|
if rules[i].ID != expectedID {
|
||||||
|
t.Errorf("rule %d ID = %q, want %q", i, rules[i].ID, expectedID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseConfigWithInclude(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
mainConfig := `
|
||||||
|
window-rule {
|
||||||
|
match app-id="mainapp"
|
||||||
|
opacity 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
include "extra.kdl"
|
||||||
|
`
|
||||||
|
extraConfig := `
|
||||||
|
window-rule {
|
||||||
|
match app-id="extraapp"
|
||||||
|
open-maximized true
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(mainConfig), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "extra.kdl"), []byte(extraConfig), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := NewNiriRulesParser(tmpDir)
|
||||||
|
rules, err := parser.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rules) != 2 {
|
||||||
|
t.Errorf("expected 2 rules, got %d", len(rules))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseSizeNode(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
config := `
|
||||||
|
window-rule {
|
||||||
|
match app-id="testapp"
|
||||||
|
default-column-width { fixed 800; }
|
||||||
|
default-window-height { proportion 0.5; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := NewNiriRulesParser(tmpDir)
|
||||||
|
rules, err := parser.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rules) != 1 {
|
||||||
|
t.Fatalf("expected 1 rule, got %d", len(rules))
|
||||||
|
}
|
||||||
|
|
||||||
|
if rules[0].DefaultColumnWidth != "fixed 800" {
|
||||||
|
t.Errorf("DefaultColumnWidth = %q, want 'fixed 800'", rules[0].DefaultColumnWidth)
|
||||||
|
}
|
||||||
|
if rules[0].DefaultWindowHeight != "proportion 0.5" {
|
||||||
|
t.Errorf("DefaultWindowHeight = %q, want 'proportion 0.5'", rules[0].DefaultWindowHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatSizeProperty(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
propName string
|
||||||
|
value string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fixed size",
|
||||||
|
propName: "default-column-width",
|
||||||
|
value: "fixed 800",
|
||||||
|
want: " default-column-width { fixed 800; }",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "proportion",
|
||||||
|
propName: "default-window-height",
|
||||||
|
value: "proportion 0.5",
|
||||||
|
want: " default-window-height { proportion 0.5; }",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid format",
|
||||||
|
propName: "default-column-width",
|
||||||
|
value: "invalid",
|
||||||
|
want: " default-column-width { }",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := formatSizeProperty(tt.propName, tt.value)
|
||||||
|
if result != tt.want {
|
||||||
|
t.Errorf("formatSizeProperty(%q, %q) = %q, want %q",
|
||||||
|
tt.propName, tt.value, result, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriDMSRulesStatus(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
config := `
|
||||||
|
window-rule {
|
||||||
|
match app-id="testapp"
|
||||||
|
opacity 0.9
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriWindowRules(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseNiriWindowRules failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.DMSStatus == nil {
|
||||||
|
t.Fatal("DMSStatus should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.DMSStatus.Exists {
|
||||||
|
t.Error("DMSStatus.Exists should be false when dms rules file doesn't exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
22
core/internal/windowrules/providers/providers_test.go
Normal file
22
core/internal/windowrules/providers/providers_test.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import "github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
|
||||||
|
|
||||||
|
func newTestWindowRule(id, name, appID string) windowrules.WindowRule {
|
||||||
|
return windowrules.WindowRule{
|
||||||
|
ID: id,
|
||||||
|
Name: name,
|
||||||
|
Enabled: true,
|
||||||
|
MatchCriteria: windowrules.MatchCriteria{
|
||||||
|
AppID: appID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolPtr(b bool) *bool {
|
||||||
|
return &b
|
||||||
|
}
|
||||||
|
|
||||||
|
func floatPtr(f float64) *float64 {
|
||||||
|
return &f
|
||||||
|
}
|
||||||
103
core/internal/windowrules/types.go
Normal file
103
core/internal/windowrules/types.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package windowrules
|
||||||
|
|
||||||
|
type MatchCriteria struct {
|
||||||
|
AppID string `json:"appId,omitempty"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
IsFloating *bool `json:"isFloating,omitempty"`
|
||||||
|
IsActive *bool `json:"isActive,omitempty"`
|
||||||
|
IsFocused *bool `json:"isFocused,omitempty"`
|
||||||
|
IsActiveInColumn *bool `json:"isActiveInColumn,omitempty"`
|
||||||
|
IsWindowCastTarget *bool `json:"isWindowCastTarget,omitempty"`
|
||||||
|
IsUrgent *bool `json:"isUrgent,omitempty"`
|
||||||
|
AtStartup *bool `json:"atStartup,omitempty"`
|
||||||
|
XWayland *bool `json:"xwayland,omitempty"`
|
||||||
|
Fullscreen *bool `json:"fullscreen,omitempty"`
|
||||||
|
Pinned *bool `json:"pinned,omitempty"`
|
||||||
|
Initialised *bool `json:"initialised,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Actions struct {
|
||||||
|
Opacity *float64 `json:"opacity,omitempty"`
|
||||||
|
OpenFloating *bool `json:"openFloating,omitempty"`
|
||||||
|
OpenMaximized *bool `json:"openMaximized,omitempty"`
|
||||||
|
OpenMaximizedToEdges *bool `json:"openMaximizedToEdges,omitempty"`
|
||||||
|
OpenFullscreen *bool `json:"openFullscreen,omitempty"`
|
||||||
|
OpenFocused *bool `json:"openFocused,omitempty"`
|
||||||
|
OpenOnOutput string `json:"openOnOutput,omitempty"`
|
||||||
|
OpenOnWorkspace string `json:"openOnWorkspace,omitempty"`
|
||||||
|
DefaultColumnWidth string `json:"defaultColumnWidth,omitempty"`
|
||||||
|
DefaultWindowHeight string `json:"defaultWindowHeight,omitempty"`
|
||||||
|
VariableRefreshRate *bool `json:"variableRefreshRate,omitempty"`
|
||||||
|
BlockOutFrom string `json:"blockOutFrom,omitempty"`
|
||||||
|
DefaultColumnDisplay string `json:"defaultColumnDisplay,omitempty"`
|
||||||
|
ScrollFactor *float64 `json:"scrollFactor,omitempty"`
|
||||||
|
CornerRadius *int `json:"cornerRadius,omitempty"`
|
||||||
|
ClipToGeometry *bool `json:"clipToGeometry,omitempty"`
|
||||||
|
TiledState *bool `json:"tiledState,omitempty"`
|
||||||
|
MinWidth *int `json:"minWidth,omitempty"`
|
||||||
|
MaxWidth *int `json:"maxWidth,omitempty"`
|
||||||
|
MinHeight *int `json:"minHeight,omitempty"`
|
||||||
|
MaxHeight *int `json:"maxHeight,omitempty"`
|
||||||
|
BorderColor string `json:"borderColor,omitempty"`
|
||||||
|
FocusRingColor string `json:"focusRingColor,omitempty"`
|
||||||
|
FocusRingOff *bool `json:"focusRingOff,omitempty"`
|
||||||
|
BorderOff *bool `json:"borderOff,omitempty"`
|
||||||
|
DrawBorderWithBg *bool `json:"drawBorderWithBackground,omitempty"`
|
||||||
|
Size string `json:"size,omitempty"`
|
||||||
|
Move string `json:"move,omitempty"`
|
||||||
|
Monitor string `json:"monitor,omitempty"`
|
||||||
|
Workspace string `json:"workspace,omitempty"`
|
||||||
|
Tile *bool `json:"tile,omitempty"`
|
||||||
|
NoFocus *bool `json:"nofocus,omitempty"`
|
||||||
|
NoBorder *bool `json:"noborder,omitempty"`
|
||||||
|
NoShadow *bool `json:"noshadow,omitempty"`
|
||||||
|
NoDim *bool `json:"nodim,omitempty"`
|
||||||
|
NoBlur *bool `json:"noblur,omitempty"`
|
||||||
|
NoAnim *bool `json:"noanim,omitempty"`
|
||||||
|
NoRounding *bool `json:"norounding,omitempty"`
|
||||||
|
Pin *bool `json:"pin,omitempty"`
|
||||||
|
Opaque *bool `json:"opaque,omitempty"`
|
||||||
|
ForcergbX *bool `json:"forcergbx,omitempty"`
|
||||||
|
Idleinhibit string `json:"idleinhibit,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowRule struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
MatchCriteria MatchCriteria `json:"matchCriteria"`
|
||||||
|
Actions Actions `json:"actions"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DMSRulesStatus struct {
|
||||||
|
Exists bool `json:"exists"`
|
||||||
|
Included bool `json:"included"`
|
||||||
|
IncludePosition int `json:"includePosition"`
|
||||||
|
TotalIncludes int `json:"totalIncludes"`
|
||||||
|
RulesAfterDMS int `json:"rulesAfterDms"`
|
||||||
|
Effective bool `json:"effective"`
|
||||||
|
OverriddenBy int `json:"overriddenBy"`
|
||||||
|
StatusMessage string `json:"statusMessage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleSet struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Rules []WindowRule `json:"rules"`
|
||||||
|
DMSRulesIncluded bool `json:"dmsRulesIncluded"`
|
||||||
|
DMSStatus *DMSRulesStatus `json:"dmsStatus,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
Name() string
|
||||||
|
GetRuleSet() (*RuleSet, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WritableProvider interface {
|
||||||
|
Provider
|
||||||
|
SetRule(rule WindowRule) error
|
||||||
|
RemoveRule(id string) error
|
||||||
|
ReorderRules(ids []string) error
|
||||||
|
GetOverridePath() string
|
||||||
|
}
|
||||||
@@ -49,12 +49,38 @@ func Normalize(v any) any {
|
|||||||
result[k] = Normalize(vv.Value())
|
result[k] = Normalize(vv.Value())
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
case map[string]any:
|
||||||
|
result := make(map[string]any)
|
||||||
|
for k, vv := range val {
|
||||||
|
result[k] = Normalize(vv)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
case map[dbus.ObjectPath]map[string]map[string]dbus.Variant:
|
||||||
|
result := make(map[string]any)
|
||||||
|
for path, ifaces := range val {
|
||||||
|
ifaceMap := make(map[string]any)
|
||||||
|
for ifaceName, props := range ifaces {
|
||||||
|
propMap := make(map[string]any)
|
||||||
|
for propName, propVal := range props {
|
||||||
|
propMap[propName] = Normalize(propVal.Value())
|
||||||
|
}
|
||||||
|
ifaceMap[ifaceName] = propMap
|
||||||
|
}
|
||||||
|
result[string(path)] = ifaceMap
|
||||||
|
}
|
||||||
|
return result
|
||||||
case []any:
|
case []any:
|
||||||
result := make([]any, len(val))
|
result := make([]any, len(val))
|
||||||
for i, item := range val {
|
for i, item := range val {
|
||||||
result[i] = Normalize(item)
|
result[i] = Normalize(item)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
case []dbus.Variant:
|
||||||
|
result := make([]any, len(val))
|
||||||
|
for i, item := range val {
|
||||||
|
result[i] = Normalize(item.Value())
|
||||||
|
}
|
||||||
|
return result
|
||||||
default:
|
default:
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,8 +109,6 @@ rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
|||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
||||||
|
|
||||||
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
|
|
||||||
|
|
||||||
%posttrans
|
%posttrans
|
||||||
# Signal running DMS instances to reload
|
# Signal running DMS instances to reload
|
||||||
pkill -USR1 -x dms >/dev/null 2>&1 || :
|
pkill -USR1 -x dms >/dev/null 2>&1 || :
|
||||||
|
|||||||
@@ -100,8 +100,6 @@ rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
|||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/core
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/core
|
||||||
|
|
||||||
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
|
|
||||||
|
|
||||||
%posttrans
|
%posttrans
|
||||||
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
||||||
|
|||||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1766651565,
|
"lastModified": 1769018530,
|
||||||
"narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=",
|
"narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539",
|
"rev": "88d3861acdd3d2f0e361767018218e51810df8a1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
kirigami.unwrapped
|
kirigami.unwrapped
|
||||||
sonnet
|
sonnet
|
||||||
qtmultimedia
|
qtmultimedia
|
||||||
|
qtimageformats
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -78,7 +79,7 @@
|
|||||||
inherit version;
|
inherit version;
|
||||||
pname = "dms-shell";
|
pname = "dms-shell";
|
||||||
src = ./core;
|
src = ./core;
|
||||||
vendorHash = "sha256-kWHB/FN6Z2Ydh+VvNrDnbg18RuJSDAle4DHDAP4NpNk=";
|
vendorHash = "sha256-vsfCgpilOHzJbTaJjJfMK/cSvtyFYJsPDjY4m3iuoFg=";
|
||||||
|
|
||||||
subPackages = [ "cmd/dms" ];
|
subPackages = [ "cmd/dms" ];
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property var facts: [
|
|
||||||
"A photon takes 100,000 to 200,000 years bouncing through the Sun's dense core, then races to Earth in just 8 minutes 20 seconds.",
|
|
||||||
"A teaspoon of neutron star matter would weigh a billion metric tons here on Earth.",
|
|
||||||
"Right now, 100 trillion solar neutrinos are passing through your body every second.",
|
|
||||||
"The Sun converts 4 million metric tons of matter into pure energy every second—enough to power Earth for 500,000 years.",
|
|
||||||
"The universe still glows with leftover heat from the Big Bang—just 2.7 degrees above absolute zero.",
|
|
||||||
"There's a nebula out there that's actually colder than empty space itself.",
|
|
||||||
"We've detected black holes crashing together by measuring spacetime stretch by less than 1/10,000th the width of a proton.",
|
|
||||||
"Fast radio bursts can release more energy in 5 milliseconds than our Sun produces in 3 days.",
|
|
||||||
"Our galaxy might be crawling with billions of rogue planets drifting alone in the dark.",
|
|
||||||
"Distant galaxies can move away from us faster than light because space itself is stretching.",
|
|
||||||
"The edge of what we can see is 46.5 billion light-years away, even though the universe is only 13.8 billion years old.",
|
|
||||||
"The universe is mostly invisible: 5% regular matter, 27% dark matter, 68% dark energy.",
|
|
||||||
"A day on Venus lasts longer than its entire year around the Sun.",
|
|
||||||
"On Mercury, the time between sunrises is 176 Earth days long.",
|
|
||||||
"In about 4.5 billion years, our galaxy will smash into Andromeda.",
|
|
||||||
"Most of the gold in your jewelry was forged when neutron stars collided somewhere in space.",
|
|
||||||
"PSR J1748-2446ad, the fastest spinning star, rotates 716 times per second—its equator moves at 24% the speed of light.",
|
|
||||||
"Cosmic rays create particles that shouldn't make it to Earth's surface, but time dilation lets them sneak through.",
|
|
||||||
"Jupiter's magnetic field is so huge that if we could see it, it would look bigger than the Moon in our sky.",
|
|
||||||
"Interstellar space is so empty it's like a cube 32 kilometers wide containing just a single grain of sand.",
|
|
||||||
"Voyager 1 is 24 billion kilometers away but won't leave the Sun's gravitational influence for another 30,000 years.",
|
|
||||||
"Counting to a billion at one number per second would take over 31 years.",
|
|
||||||
"Space is so vast, even speeding at light-speed, you'd never return past the cosmic horizon.",
|
|
||||||
"Astronauts on the ISS age about 0.01 seconds less each year than people on Earth.",
|
|
||||||
"Sagittarius B2, a dust cloud near our galaxy's center, contains ethyl formate—the compound that gives raspberries their flavor and rum its smell.",
|
|
||||||
"Beyond 16 billion light-years, the cosmic event horizon marks where space expands too fast for light to ever reach us again.",
|
|
||||||
"Even at light-speed, you'd never catch up to most galaxies—space expands faster.",
|
|
||||||
"Only around 5% of galaxies are ever reachable—even at light-speed.",
|
|
||||||
"If the Sun vanished, we'd still orbit it for 8 minutes before drifting away.",
|
|
||||||
"If a planet 65 million light-years away looked at Earth now, it'd see dinosaurs.",
|
|
||||||
"Our oldest radio signals will reach the Milky Way's center in 26,000 years.",
|
|
||||||
"Every atom in your body heavier than hydrogen was forged in the nuclear furnace of a dying star.",
|
|
||||||
"The Moon moves 3.8 centimeters farther from Earth every year.",
|
|
||||||
"The universe creates 275 million new stars every single day.",
|
|
||||||
"Jupiter's Great Red Spot is a storm twice the size of Earth that has been raging for at least 350 years.",
|
|
||||||
"If you watched someone fall into a black hole, they'd appear frozen at the event horizon forever—time effectively stops from your perspective.",
|
|
||||||
"The Boötes Supervoid is a cosmic desert 1.8 billion light-years across with 60% fewer galaxies than it should have."
|
|
||||||
]
|
|
||||||
|
|
||||||
function getRandomFact() {
|
|
||||||
return facts[Math.floor(Math.random() * facts.length)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -100,7 +100,8 @@ const DMS_ACTIONS = [
|
|||||||
{ id: "spawn dms ipc call hypr openOverview", label: "Hyprland: Open Overview", compositor: "hyprland" },
|
{ id: "spawn dms ipc call hypr openOverview", label: "Hyprland: Open Overview", compositor: "hyprland" },
|
||||||
{ id: "spawn dms ipc call hypr closeOverview", label: "Hyprland: Close Overview", compositor: "hyprland" },
|
{ id: "spawn dms ipc call hypr closeOverview", label: "Hyprland: Close Overview", compositor: "hyprland" },
|
||||||
{ id: "spawn dms ipc call wallpaper next", label: "Wallpaper: Next" },
|
{ id: "spawn dms ipc call wallpaper next", label: "Wallpaper: Next" },
|
||||||
{ id: "spawn dms ipc call wallpaper prev", label: "Wallpaper: Previous" }
|
{ id: "spawn dms ipc call wallpaper prev", label: "Wallpaper: Previous" },
|
||||||
|
{ id: "spawn dms ipc call workspace-rename open", label: "Workspace: Rename" }
|
||||||
];
|
];
|
||||||
|
|
||||||
const NIRI_ACTIONS = {
|
const NIRI_ACTIONS = {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import "settings/SessionStore.js" as Store
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property int sessionConfigVersion: 2
|
readonly property int sessionConfigVersion: 3
|
||||||
|
|
||||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||||
property bool _parseError: false
|
property bool _parseError: false
|
||||||
@@ -82,6 +82,15 @@ Singleton {
|
|||||||
property bool nightModeUseIPLocation: false
|
property bool nightModeUseIPLocation: false
|
||||||
property string nightModeLocationProvider: ""
|
property string nightModeLocationProvider: ""
|
||||||
|
|
||||||
|
property bool themeModeAutoEnabled: false
|
||||||
|
property string themeModeAutoMode: "time"
|
||||||
|
property int themeModeStartHour: 18
|
||||||
|
property int themeModeStartMinute: 0
|
||||||
|
property int themeModeEndHour: 6
|
||||||
|
property int themeModeEndMinute: 0
|
||||||
|
property bool themeModeShareGammaSettings: true
|
||||||
|
property string themeModeNextTransition: ""
|
||||||
|
|
||||||
property var pinnedApps: []
|
property var pinnedApps: []
|
||||||
property var barPinnedApps: []
|
property var barPinnedApps: []
|
||||||
property int dockLauncherPosition: 0
|
property int dockLauncherPosition: 0
|
||||||
@@ -109,6 +118,8 @@ Singleton {
|
|||||||
property var appOverrides: ({})
|
property var appOverrides: ({})
|
||||||
property bool searchAppActions: true
|
property bool searchAppActions: true
|
||||||
|
|
||||||
|
property string vpnLastConnected: ""
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
@@ -172,7 +183,7 @@ Singleton {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
_parseError = true;
|
_parseError = true;
|
||||||
const msg = e.message;
|
const msg = e.message;
|
||||||
console.error("SessionData: Failed to parse session.json - file will not be overwritten. Error:", msg);
|
console.error("SessionData: Failed to parse session.json - file will not be overwritten.");
|
||||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
|
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,14 +197,10 @@ Singleton {
|
|||||||
_isReadOnly = !writable;
|
_isReadOnly = !writable;
|
||||||
if (_isReadOnly) {
|
if (_isReadOnly) {
|
||||||
_hasUnsavedChanges = _checkForUnsavedChanges();
|
_hasUnsavedChanges = _checkForUnsavedChanges();
|
||||||
if (!wasReadOnly)
|
|
||||||
console.info("SessionData: session.json is now read-only");
|
|
||||||
} else {
|
} else {
|
||||||
_loadedSessionSnapshot = getCurrentSessionJson();
|
_loadedSessionSnapshot = getCurrentSessionJson();
|
||||||
_hasUnsavedChanges = false;
|
_hasUnsavedChanges = false;
|
||||||
if (wasReadOnly)
|
if (wasReadOnly && _pendingMigration)
|
||||||
console.info("SessionData: session.json is now writable");
|
|
||||||
if (_pendingMigration)
|
|
||||||
settingsFile.setText(JSON.stringify(_pendingMigration, null, 2));
|
settingsFile.setText(JSON.stringify(_pendingMigration, null, 2));
|
||||||
}
|
}
|
||||||
_pendingMigration = null;
|
_pendingMigration = null;
|
||||||
@@ -255,7 +262,7 @@ Singleton {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
_parseError = true;
|
_parseError = true;
|
||||||
const msg = e.message;
|
const msg = e.message;
|
||||||
console.error("SessionData: Failed to parse session.json - file will not be overwritten. Error:", msg);
|
console.error("SessionData: Failed to parse session.json - file will not be overwritten.");
|
||||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
|
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,7 +280,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function migrateFromUndefinedToV1(settings) {
|
function migrateFromUndefinedToV1(settings) {
|
||||||
console.info("SessionData: Migrating configuration from undefined to version 1");
|
|
||||||
if (typeof SettingsData !== "undefined") {
|
if (typeof SettingsData !== "undefined") {
|
||||||
if (settings.acMonitorTimeout !== undefined) {
|
if (settings.acMonitorTimeout !== undefined) {
|
||||||
SettingsData.set("acMonitorTimeout", settings.acMonitorTimeout);
|
SettingsData.set("acMonitorTimeout", settings.acMonitorTimeout);
|
||||||
@@ -448,7 +454,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!screen) {
|
if (!screen) {
|
||||||
console.warn("SessionData: Screen not found:", screenName);
|
console.warn("SessionData: Screen not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,7 +551,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!screen) {
|
if (!screen) {
|
||||||
console.warn("SessionData: Screen not found:", screenName);
|
console.warn("SessionData: Screen not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,7 +589,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!screen) {
|
if (!screen) {
|
||||||
console.warn("SessionData: Screen not found:", screenName);
|
console.warn("SessionData: Screen not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,7 +627,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!screen) {
|
if (!screen) {
|
||||||
console.warn("SessionData: Screen not found:", screenName);
|
console.warn("SessionData: Screen not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,7 +665,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!screen) {
|
if (!screen) {
|
||||||
console.warn("SessionData: Screen not found:", screenName);
|
console.warn("SessionData: Screen not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,7 +708,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setNightModeAutoEnabled(enabled) {
|
function setNightModeAutoEnabled(enabled) {
|
||||||
console.log("SessionData: Setting nightModeAutoEnabled to", enabled);
|
|
||||||
nightModeAutoEnabled = enabled;
|
nightModeAutoEnabled = enabled;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
@@ -738,13 +743,11 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setLatitude(lat) {
|
function setLatitude(lat) {
|
||||||
console.log("SessionData: Setting latitude to", lat);
|
|
||||||
latitude = lat;
|
latitude = lat;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLongitude(lng) {
|
function setLongitude(lng) {
|
||||||
console.log("SessionData: Setting longitude to", lng);
|
|
||||||
longitude = lng;
|
longitude = lng;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
@@ -754,6 +757,41 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setThemeModeAutoEnabled(enabled) {
|
||||||
|
themeModeAutoEnabled = enabled;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setThemeModeAutoMode(mode) {
|
||||||
|
themeModeAutoMode = mode;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setThemeModeStartHour(hour) {
|
||||||
|
themeModeStartHour = hour;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setThemeModeStartMinute(minute) {
|
||||||
|
themeModeStartMinute = minute;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setThemeModeEndHour(hour) {
|
||||||
|
themeModeEndHour = hour;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setThemeModeEndMinute(minute) {
|
||||||
|
themeModeEndMinute = minute;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setThemeModeShareGammaSettings(share) {
|
||||||
|
themeModeShareGammaSettings = share;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
function setPinnedApps(apps) {
|
function setPinnedApps(apps) {
|
||||||
pinnedApps = apps;
|
pinnedApps = apps;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -1003,6 +1041,11 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setVpnLastConnected(uuid) {
|
||||||
|
vpnLastConnected = uuid || "";
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
function syncWallpaperForCurrentMode() {
|
function syncWallpaperForCurrentMode() {
|
||||||
if (!perModeWallpaper)
|
if (!perModeWallpaper)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ Singleton {
|
|||||||
property real dockTransparency: 1
|
property real dockTransparency: 1
|
||||||
property string widgetBackgroundColor: "sch"
|
property string widgetBackgroundColor: "sch"
|
||||||
property string widgetColorMode: "default"
|
property string widgetColorMode: "default"
|
||||||
|
property string controlCenterTileColorMode: "primary"
|
||||||
property real cornerRadius: 12
|
property real cornerRadius: 12
|
||||||
property int niriLayoutGapsOverride: -1
|
property int niriLayoutGapsOverride: -1
|
||||||
property int niriLayoutRadiusOverride: -1
|
property int niriLayoutRadiusOverride: -1
|
||||||
@@ -146,6 +147,7 @@ Singleton {
|
|||||||
|
|
||||||
property bool use24HourClock: true
|
property bool use24HourClock: true
|
||||||
property bool showSeconds: false
|
property bool showSeconds: false
|
||||||
|
property bool padHours12Hour: false
|
||||||
property bool useFahrenheit: false
|
property bool useFahrenheit: false
|
||||||
property string windSpeedUnit: "kmh"
|
property string windSpeedUnit: "kmh"
|
||||||
property bool nightModeEnabled: false
|
property bool nightModeEnabled: false
|
||||||
@@ -241,6 +243,7 @@ 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 int workspaceAppIconSizeOffset: 0
|
||||||
property bool workspaceFollowFocus: false
|
property bool workspaceFollowFocus: false
|
||||||
property bool showOccupiedWorkspacesOnly: false
|
property bool showOccupiedWorkspacesOnly: false
|
||||||
property bool reverseScrolling: false
|
property bool reverseScrolling: false
|
||||||
@@ -260,6 +263,9 @@ Singleton {
|
|||||||
property bool clockCompactMode: false
|
property bool clockCompactMode: false
|
||||||
property bool focusedWindowCompactMode: false
|
property bool focusedWindowCompactMode: false
|
||||||
property bool runningAppsCompactMode: true
|
property bool runningAppsCompactMode: true
|
||||||
|
property int barMaxVisibleApps: 0
|
||||||
|
property int barMaxVisibleRunningApps: 0
|
||||||
|
property bool barShowOverflowBadge: true
|
||||||
property bool keyboardLayoutNameCompactMode: false
|
property bool keyboardLayoutNameCompactMode: false
|
||||||
property bool runningAppsCurrentWorkspace: false
|
property bool runningAppsCurrentWorkspace: false
|
||||||
property bool runningAppsGroupByApp: false
|
property bool runningAppsGroupByApp: false
|
||||||
@@ -273,6 +279,8 @@ Singleton {
|
|||||||
property string spotlightModalViewMode: "list"
|
property string spotlightModalViewMode: "list"
|
||||||
property string browserPickerViewMode: "grid"
|
property string browserPickerViewMode: "grid"
|
||||||
property var browserUsageHistory: ({})
|
property var browserUsageHistory: ({})
|
||||||
|
property string appPickerViewMode: "grid"
|
||||||
|
property var filePickerUsageHistory: ({})
|
||||||
property bool sortAppsAlphabetically: false
|
property bool sortAppsAlphabetically: false
|
||||||
property int appLauncherGridColumns: 4
|
property int appLauncherGridColumns: 4
|
||||||
property bool spotlightCloseNiriOverview: true
|
property bool spotlightCloseNiriOverview: true
|
||||||
@@ -289,13 +297,13 @@ Singleton {
|
|||||||
|
|
||||||
property string _legacyWeatherLocation: "New York, NY"
|
property string _legacyWeatherLocation: "New York, NY"
|
||||||
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
||||||
|
property string _legacyVpnLastConnected: ""
|
||||||
readonly property string weatherLocation: SessionData.weatherLocation
|
readonly property string weatherLocation: SessionData.weatherLocation
|
||||||
readonly property string weatherCoordinates: SessionData.weatherCoordinates
|
readonly property string weatherCoordinates: SessionData.weatherCoordinates
|
||||||
property bool useAutoLocation: false
|
property bool useAutoLocation: false
|
||||||
property bool weatherEnabled: true
|
property bool weatherEnabled: true
|
||||||
|
|
||||||
property string networkPreference: "auto"
|
property string networkPreference: "auto"
|
||||||
property string vpnLastConnected: ""
|
|
||||||
|
|
||||||
property string iconTheme: "System Default"
|
property string iconTheme: "System Default"
|
||||||
property var availableIconThemes: ["System Default"]
|
property var availableIconThemes: ["System Default"]
|
||||||
@@ -438,6 +446,9 @@ Singleton {
|
|||||||
property int dockLauncherLogoSizeOffset: 0
|
property int dockLauncherLogoSizeOffset: 0
|
||||||
property real dockLauncherLogoBrightness: 0.5
|
property real dockLauncherLogoBrightness: 0.5
|
||||||
property real dockLauncherLogoContrast: 1
|
property real dockLauncherLogoContrast: 1
|
||||||
|
property int dockMaxVisibleApps: 0
|
||||||
|
property int dockMaxVisibleRunningApps: 0
|
||||||
|
property bool dockShowOverflowBadge: true
|
||||||
|
|
||||||
property bool notificationOverlayEnabled: false
|
property bool notificationOverlayEnabled: false
|
||||||
property int overviewRows: 2
|
property int overviewRows: 2
|
||||||
@@ -452,6 +463,7 @@ Singleton {
|
|||||||
property bool lockScreenShowDate: true
|
property bool lockScreenShowDate: true
|
||||||
property bool lockScreenShowProfileImage: true
|
property bool lockScreenShowProfileImage: true
|
||||||
property bool lockScreenShowPasswordField: true
|
property bool lockScreenShowPasswordField: true
|
||||||
|
property bool lockScreenShowMediaPlayer: true
|
||||||
property bool lockScreenPowerOffMonitorsOnLock: false
|
property bool lockScreenPowerOffMonitorsOnLock: false
|
||||||
|
|
||||||
property bool enableFprint: false
|
property bool enableFprint: false
|
||||||
@@ -507,6 +519,11 @@ Singleton {
|
|||||||
property var showOnLastDisplay: ({})
|
property var showOnLastDisplay: ({})
|
||||||
property var niriOutputSettings: ({})
|
property var niriOutputSettings: ({})
|
||||||
property var hyprlandOutputSettings: ({})
|
property var hyprlandOutputSettings: ({})
|
||||||
|
property var displayProfiles: ({})
|
||||||
|
property var activeDisplayProfile: ({})
|
||||||
|
property bool displayProfileAutoSelect: false
|
||||||
|
property bool displayShowDisconnected: false
|
||||||
|
property bool displaySnapToEdge: true
|
||||||
|
|
||||||
property var barConfigs: [
|
property var barConfigs: [
|
||||||
{
|
{
|
||||||
@@ -998,7 +1015,6 @@ Singleton {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
rm -rf ~/.cache/icon-cache ~/.cache/thumbnails 2>/dev/null || true
|
|
||||||
pkill -HUP -f 'gtk' 2>/dev/null || true`;
|
pkill -HUP -f 'gtk' 2>/dev/null || true`;
|
||||||
|
|
||||||
Quickshell.execDetached(["sh", "-lc", configScript]);
|
Quickshell.execDetached(["sh", "-lc", configScript]);
|
||||||
@@ -1030,8 +1046,7 @@ Singleton {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
update_qt_icon_theme ${_configDir}/qt5ct/qt5ct.conf '${qtThemeNameEscaped}'
|
update_qt_icon_theme ${_configDir}/qt5ct/qt5ct.conf '${qtThemeNameEscaped}'
|
||||||
update_qt_icon_theme ${_configDir}/qt6ct/qt6ct.conf '${qtThemeNameEscaped}'
|
update_qt_icon_theme ${_configDir}/qt6ct/qt6ct.conf '${qtThemeNameEscaped}'`;
|
||||||
rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || true`;
|
|
||||||
|
|
||||||
Quickshell.execDetached(["sh", "-lc", script]);
|
Quickshell.execDetached(["sh", "-lc", script]);
|
||||||
}
|
}
|
||||||
@@ -1074,11 +1089,15 @@ Singleton {
|
|||||||
_legacyWeatherLocation = obj.weatherLocation;
|
_legacyWeatherLocation = obj.weatherLocation;
|
||||||
if (obj?.weatherCoordinates !== undefined)
|
if (obj?.weatherCoordinates !== undefined)
|
||||||
_legacyWeatherCoordinates = obj.weatherCoordinates;
|
_legacyWeatherCoordinates = obj.weatherCoordinates;
|
||||||
|
if (obj?.vpnLastConnected !== undefined && obj.vpnLastConnected !== "") {
|
||||||
|
_legacyVpnLastConnected = obj.vpnLastConnected;
|
||||||
|
SessionData.vpnLastConnected = _legacyVpnLastConnected;
|
||||||
|
SessionData.saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
|
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
|
||||||
_hasLoaded = true;
|
_hasLoaded = true;
|
||||||
applyStoredTheme();
|
applyStoredTheme();
|
||||||
applyStoredIconTheme();
|
|
||||||
updateCompositorCursor();
|
updateCompositorCursor();
|
||||||
Processes.detectQtTools();
|
Processes.detectQtTools();
|
||||||
|
|
||||||
@@ -1089,7 +1108,6 @@ Singleton {
|
|||||||
console.error("SettingsData: Failed to parse settings.json - file will not be overwritten. Error:", msg);
|
console.error("SettingsData: Failed to parse settings.json - file will not be overwritten. Error:", msg);
|
||||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse settings.json"), msg));
|
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse settings.json"), msg));
|
||||||
applyStoredTheme();
|
applyStoredTheme();
|
||||||
applyStoredIconTheme();
|
|
||||||
} finally {
|
} finally {
|
||||||
_loading = false;
|
_loading = false;
|
||||||
}
|
}
|
||||||
@@ -1251,11 +1269,11 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getEffectiveTimeFormat() {
|
function getEffectiveTimeFormat() {
|
||||||
if (use24HourClock) {
|
if (use24HourClock)
|
||||||
return showSeconds ? "hh:mm:ss" : "hh:mm";
|
return showSeconds ? "hh:mm:ss" : "hh:mm";
|
||||||
} else {
|
if (padHours12Hour)
|
||||||
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
|
return showSeconds ? "hh:mm:ss AP" : "hh:mm AP";
|
||||||
}
|
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEffectiveClockDateFormat() {
|
function getEffectiveClockDateFormat() {
|
||||||
@@ -2243,6 +2261,39 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDisplayProfiles(compositor) {
|
||||||
|
return displayProfiles[compositor] || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDisplayProfile(compositor, profileId, data) {
|
||||||
|
const updated = JSON.parse(JSON.stringify(displayProfiles));
|
||||||
|
if (!updated[compositor])
|
||||||
|
updated[compositor] = {};
|
||||||
|
updated[compositor][profileId] = data;
|
||||||
|
displayProfiles = updated;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDisplayProfile(compositor, profileId) {
|
||||||
|
if (!displayProfiles[compositor] || !displayProfiles[compositor][profileId])
|
||||||
|
return;
|
||||||
|
const updated = JSON.parse(JSON.stringify(displayProfiles));
|
||||||
|
delete updated[compositor][profileId];
|
||||||
|
displayProfiles = updated;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveDisplayProfile(compositor) {
|
||||||
|
return activeDisplayProfile[compositor] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveDisplayProfile(compositor, profileId) {
|
||||||
|
const updated = JSON.parse(JSON.stringify(activeDisplayProfile));
|
||||||
|
updated[compositor] = profileId;
|
||||||
|
activeDisplayProfile = updated;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
ListModel {
|
ListModel {
|
||||||
id: leftWidgetsModel
|
id: leftWidgetsModel
|
||||||
}
|
}
|
||||||
@@ -2307,11 +2358,15 @@ Singleton {
|
|||||||
_legacyWeatherLocation = obj.weatherLocation;
|
_legacyWeatherLocation = obj.weatherLocation;
|
||||||
if (obj.weatherCoordinates !== undefined)
|
if (obj.weatherCoordinates !== undefined)
|
||||||
_legacyWeatherCoordinates = obj.weatherCoordinates;
|
_legacyWeatherCoordinates = obj.weatherCoordinates;
|
||||||
|
if (obj.vpnLastConnected !== undefined && obj.vpnLastConnected !== "") {
|
||||||
|
_legacyVpnLastConnected = obj.vpnLastConnected;
|
||||||
|
SessionData.vpnLastConnected = _legacyVpnLastConnected;
|
||||||
|
SessionData.saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
|
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
|
||||||
_hasLoaded = true;
|
_hasLoaded = true;
|
||||||
applyStoredTheme();
|
applyStoredTheme();
|
||||||
applyStoredIconTheme();
|
|
||||||
updateCompositorCursor();
|
updateCompositorCursor();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_parseError = true;
|
_parseError = true;
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ Singleton {
|
|||||||
property var matugenColors: ({})
|
property var matugenColors: ({})
|
||||||
property var _pendingGenerateParams: null
|
property var _pendingGenerateParams: null
|
||||||
|
|
||||||
|
property bool themeModeAutomationActive: false
|
||||||
|
property bool dmsServiceWasDisconnected: true
|
||||||
|
|
||||||
readonly property var dank16: {
|
readonly property var dank16: {
|
||||||
const raw = matugenColors?.dank16;
|
const raw = matugenColors?.dank16;
|
||||||
if (!raw)
|
if (!raw)
|
||||||
@@ -176,6 +179,227 @@ Singleton {
|
|||||||
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
|
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
|
||||||
switchTheme(SettingsData.currentThemeName, false, false);
|
switchTheme(SettingsData.currentThemeName, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) {
|
||||||
|
startThemeModeAutomation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SessionData
|
||||||
|
enabled: typeof SessionData !== "undefined"
|
||||||
|
|
||||||
|
function onThemeModeAutoEnabledChanged() {
|
||||||
|
if (SessionData.themeModeAutoEnabled) {
|
||||||
|
root.startThemeModeAutomation();
|
||||||
|
} else {
|
||||||
|
root.stopThemeModeAutomation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onThemeModeAutoModeChanged() {
|
||||||
|
if (root.themeModeAutomationActive) {
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
root.syncTimeThemeSchedule();
|
||||||
|
root.syncLocationThemeSchedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onThemeModeStartHourChanged() {
|
||||||
|
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
root.syncTimeThemeSchedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onThemeModeStartMinuteChanged() {
|
||||||
|
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
root.syncTimeThemeSchedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onThemeModeEndHourChanged() {
|
||||||
|
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
root.syncTimeThemeSchedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onThemeModeEndMinuteChanged() {
|
||||||
|
if (root.themeModeAutomationActive && !SessionData.themeModeShareGammaSettings) {
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
root.syncTimeThemeSchedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onThemeModeShareGammaSettingsChanged() {
|
||||||
|
if (root.themeModeAutomationActive) {
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
root.syncTimeThemeSchedule();
|
||||||
|
root.syncLocationThemeSchedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNightModeStartHourChanged() {
|
||||||
|
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
root.syncTimeThemeSchedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNightModeStartMinuteChanged() {
|
||||||
|
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
root.syncTimeThemeSchedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNightModeEndHourChanged() {
|
||||||
|
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
root.syncTimeThemeSchedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNightModeEndMinuteChanged() {
|
||||||
|
if (root.themeModeAutomationActive && SessionData.themeModeShareGammaSettings) {
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
root.syncTimeThemeSchedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLatitudeChanged() {
|
||||||
|
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
|
||||||
|
if (!SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0 && typeof DMSService !== "undefined") {
|
||||||
|
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||||
|
"latitude": SessionData.latitude,
|
||||||
|
"longitude": SessionData.longitude
|
||||||
|
});
|
||||||
|
}
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
root.syncLocationThemeSchedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLongitudeChanged() {
|
||||||
|
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
|
||||||
|
if (!SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0 && typeof DMSService !== "undefined") {
|
||||||
|
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||||
|
"latitude": SessionData.latitude,
|
||||||
|
"longitude": SessionData.longitude
|
||||||
|
});
|
||||||
|
}
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
root.syncLocationThemeSchedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNightModeUseIPLocationChanged() {
|
||||||
|
if (root.themeModeAutomationActive && SessionData.themeModeAutoMode === "location") {
|
||||||
|
if (typeof DMSService !== "undefined") {
|
||||||
|
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||||
|
"use": SessionData.nightModeUseIPLocation
|
||||||
|
}, response => {
|
||||||
|
if (!response.error && !SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||||
|
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||||
|
"latitude": SessionData.latitude,
|
||||||
|
"longitude": SessionData.longitude
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
root.syncLocationThemeSchedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// React to gamma backend's isDay state changes for location-based mode
|
||||||
|
Connections {
|
||||||
|
target: DisplayService
|
||||||
|
enabled: typeof DisplayService !== "undefined" && typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "location" && !themeAutoBackendAvailable()
|
||||||
|
|
||||||
|
function onGammaIsDayChanged() {
|
||||||
|
if (root.isLightMode !== DisplayService.gammaIsDay) {
|
||||||
|
root.setLightMode(DisplayService.gammaIsDay, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: DMSService
|
||||||
|
enabled: typeof DMSService !== "undefined" && typeof SessionData !== "undefined"
|
||||||
|
|
||||||
|
function onLoginctlEvent(event) {
|
||||||
|
if (!SessionData.themeModeAutoEnabled)
|
||||||
|
return;
|
||||||
|
if (event.event === "unlock" || event.event === "resume") {
|
||||||
|
if (!themeAutoBackendAvailable()) {
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DMSService.sendRequest("theme.auto.trigger", {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onThemeAutoStateUpdate(data) {
|
||||||
|
if (!SessionData.themeModeAutoEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyThemeAutoState(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onConnectionStateChanged() {
|
||||||
|
if (DMSService.isConnected && SessionData.themeModeAutoMode === "time") {
|
||||||
|
root.syncTimeThemeSchedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DMSService.isConnected && SessionData.themeModeAutoMode === "location") {
|
||||||
|
root.syncLocationThemeSchedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeAutoBackendAvailable() && SessionData.themeModeAutoEnabled) {
|
||||||
|
DMSService.sendRequest("theme.auto.getState", null, response => {
|
||||||
|
if (response && response.result) {
|
||||||
|
applyThemeAutoState(response.result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SessionData.themeModeAutoEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DMSService.isConnected && SessionData.themeModeAutoMode === "location") {
|
||||||
|
if (SessionData.nightModeUseIPLocation) {
|
||||||
|
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||||
|
"use": true
|
||||||
|
}, response => {
|
||||||
|
if (!response.error) {
|
||||||
|
console.info("Theme automation: IP location enabled after connection");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||||
|
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||||
|
"use": false
|
||||||
|
}, response => {
|
||||||
|
if (!response.error) {
|
||||||
|
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||||
|
"latitude": SessionData.latitude,
|
||||||
|
"longitude": SessionData.longitude
|
||||||
|
}, locationResponse => {
|
||||||
|
if (locationResponse?.error) {
|
||||||
|
console.warn("Theme automation: Failed to set location", locationResponse.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn("Theme automation: No location configured");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyGreeterTheme(themeName) {
|
function applyGreeterTheme(themeName) {
|
||||||
@@ -228,39 +452,39 @@ Singleton {
|
|||||||
|
|
||||||
readonly property var availableMatugenSchemes: [({
|
readonly property var availableMatugenSchemes: [({
|
||||||
"value": "scheme-tonal-spot",
|
"value": "scheme-tonal-spot",
|
||||||
"label": "Tonal Spot",
|
"label": I18n.tr("Tonal Spot", "matugen color scheme option"),
|
||||||
"description": I18n.tr("Balanced palette with focused accents (default).")
|
"description": I18n.tr("Balanced palette with focused accents (default).")
|
||||||
}), ({
|
}), ({
|
||||||
"value": "scheme-vibrant",
|
"value": "scheme-vibrant",
|
||||||
"label": "Vibrant",
|
"label": I18n.tr("Vibrant", "matugen color scheme option"),
|
||||||
"description": I18n.tr("Lively palette with saturated accents.")
|
"description": I18n.tr("Lively palette with saturated accents.")
|
||||||
}), ({
|
}), ({
|
||||||
"value": "scheme-content",
|
"value": "scheme-content",
|
||||||
"label": "Content",
|
"label": I18n.tr("Content", "matugen color scheme option"),
|
||||||
"description": I18n.tr("Derives colors that closely match the underlying image.")
|
"description": I18n.tr("Derives colors that closely match the underlying image.")
|
||||||
}), ({
|
}), ({
|
||||||
"value": "scheme-expressive",
|
"value": "scheme-expressive",
|
||||||
"label": "Expressive",
|
"label": I18n.tr("Expressive", "matugen color scheme option"),
|
||||||
"description": I18n.tr("Vibrant palette with playful saturation.")
|
"description": I18n.tr("Vibrant palette with playful saturation.")
|
||||||
}), ({
|
}), ({
|
||||||
"value": "scheme-fidelity",
|
"value": "scheme-fidelity",
|
||||||
"label": "Fidelity",
|
"label": I18n.tr("Fidelity", "matugen color scheme option"),
|
||||||
"description": I18n.tr("High-fidelity palette that preserves source hues.")
|
"description": I18n.tr("High-fidelity palette that preserves source hues.")
|
||||||
}), ({
|
}), ({
|
||||||
"value": "scheme-fruit-salad",
|
"value": "scheme-fruit-salad",
|
||||||
"label": "Fruit Salad",
|
"label": I18n.tr("Fruit Salad", "matugen color scheme option"),
|
||||||
"description": I18n.tr("Colorful mix of bright contrasting accents.")
|
"description": I18n.tr("Colorful mix of bright contrasting accents.")
|
||||||
}), ({
|
}), ({
|
||||||
"value": "scheme-monochrome",
|
"value": "scheme-monochrome",
|
||||||
"label": "Monochrome",
|
"label": I18n.tr("Monochrome", "matugen color scheme option"),
|
||||||
"description": I18n.tr("Minimal palette built around a single hue.")
|
"description": I18n.tr("Minimal palette built around a single hue.")
|
||||||
}), ({
|
}), ({
|
||||||
"value": "scheme-neutral",
|
"value": "scheme-neutral",
|
||||||
"label": "Neutral",
|
"label": I18n.tr("Neutral", "matugen color scheme option"),
|
||||||
"description": I18n.tr("Muted palette with subdued, calming tones.")
|
"description": I18n.tr("Muted palette with subdued, calming tones.")
|
||||||
}), ({
|
}), ({
|
||||||
"value": "scheme-rainbow",
|
"value": "scheme-rainbow",
|
||||||
"label": "Rainbow",
|
"label": I18n.tr("Rainbow", "matugen color scheme option"),
|
||||||
"description": I18n.tr("Diverse palette spanning the full spectrum.")
|
"description": I18n.tr("Diverse palette spanning the full spectrum.")
|
||||||
})]
|
})]
|
||||||
|
|
||||||
@@ -330,6 +554,58 @@ Singleton {
|
|||||||
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
|
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
|
||||||
property color errorPressed: Qt.rgba(error.r, error.g, error.b, 0.16)
|
property color errorPressed: Qt.rgba(error.r, error.g, error.b, 0.16)
|
||||||
|
|
||||||
|
readonly property color ccTileActiveBg: {
|
||||||
|
switch (SettingsData.controlCenterTileColorMode) {
|
||||||
|
case "primaryContainer":
|
||||||
|
return primaryContainer;
|
||||||
|
case "secondary":
|
||||||
|
return secondary;
|
||||||
|
case "surfaceVariant":
|
||||||
|
return surfaceVariant;
|
||||||
|
default:
|
||||||
|
return primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property color ccTileActiveText: {
|
||||||
|
switch (SettingsData.controlCenterTileColorMode) {
|
||||||
|
case "primaryContainer":
|
||||||
|
return primary;
|
||||||
|
case "secondary":
|
||||||
|
return surfaceText;
|
||||||
|
case "surfaceVariant":
|
||||||
|
return surfaceText;
|
||||||
|
default:
|
||||||
|
return primaryText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property color ccTileInactiveIcon: {
|
||||||
|
switch (SettingsData.controlCenterTileColorMode) {
|
||||||
|
case "primaryContainer":
|
||||||
|
return primary;
|
||||||
|
case "secondary":
|
||||||
|
return secondary;
|
||||||
|
case "surfaceVariant":
|
||||||
|
return surfaceText;
|
||||||
|
default:
|
||||||
|
return primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property color ccTileRing: {
|
||||||
|
switch (SettingsData.controlCenterTileColorMode) {
|
||||||
|
case "primaryContainer":
|
||||||
|
return Qt.rgba(primary.r, primary.g, primary.b, 0.22);
|
||||||
|
case "secondary":
|
||||||
|
return Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.22);
|
||||||
|
case "surfaceVariant":
|
||||||
|
return Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.22);
|
||||||
|
default:
|
||||||
|
return Qt.rgba(primaryText.r, primaryText.g, primaryText.b, 0.22);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
|
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
|
||||||
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
|
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
|
||||||
|
|
||||||
@@ -491,7 +767,9 @@ Singleton {
|
|||||||
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0
|
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0
|
||||||
|
|
||||||
function screenTransition() {
|
function screenTransition() {
|
||||||
CompositorService.isNiri && NiriService.doScreenTransition();
|
if (CompositorService.isNiri) {
|
||||||
|
NiriService.doScreenTransition();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchTheme(themeName, savePrefs = true, enableTransition = true) {
|
function switchTheme(themeName, savePrefs = true, enableTransition = true) {
|
||||||
@@ -543,8 +821,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
|
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
|
||||||
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 because, our script runner will do it.
|
// Skip with matugen because, our script runner will do it.
|
||||||
if (!matugenAvailable) {
|
if (!matugenAvailable) {
|
||||||
@@ -822,26 +1102,26 @@ Singleton {
|
|||||||
function getPowerProfileLabel(profile) {
|
function getPowerProfileLabel(profile) {
|
||||||
switch (profile) {
|
switch (profile) {
|
||||||
case 0:
|
case 0:
|
||||||
return "Power Saver";
|
return I18n.tr("Power Saver", "power profile option");
|
||||||
case 1:
|
case 1:
|
||||||
return "Balanced";
|
return I18n.tr("Balanced", "power profile option");
|
||||||
case 2:
|
case 2:
|
||||||
return "Performance";
|
return I18n.tr("Performance", "power profile option");
|
||||||
default:
|
default:
|
||||||
return "Unknown";
|
return I18n.tr("Unknown", "power profile option");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPowerProfileDescription(profile) {
|
function getPowerProfileDescription(profile) {
|
||||||
switch (profile) {
|
switch (profile) {
|
||||||
case 0:
|
case 0:
|
||||||
return "Extend battery life";
|
return I18n.tr("Extend battery life", "power profile description");
|
||||||
case 1:
|
case 1:
|
||||||
return "Balance power and performance";
|
return I18n.tr("Balance power and performance", "power profile description");
|
||||||
case 2:
|
case 2:
|
||||||
return "Prioritize performance";
|
return I18n.tr("Prioritize performance", "power profile description");
|
||||||
default:
|
default:
|
||||||
return "Custom power profile";
|
return I18n.tr("Custom power profile", "power profile description");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -948,7 +1228,7 @@ Singleton {
|
|||||||
skipTemplates.push("kcolorscheme");
|
skipTemplates.push("kcolorscheme");
|
||||||
if (!SettingsData.matugenTemplateVscode)
|
if (!SettingsData.matugenTemplateVscode)
|
||||||
skipTemplates.push("vscode");
|
skipTemplates.push("vscode");
|
||||||
if (!SettingsData.matugenTemplateEmacs)
|
if (!SettingsData.matugenTemplateEmacs)
|
||||||
skipTemplates.push("emacs");
|
skipTemplates.push("emacs");
|
||||||
}
|
}
|
||||||
if (skipTemplates.length > 0) {
|
if (skipTemplates.length > 0) {
|
||||||
@@ -1233,7 +1513,7 @@ Singleton {
|
|||||||
return `#${invR}${invG}${invB}`;
|
return `#${invR}${invG}${invB}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
property string baseLogoColor: {
|
property var baseLogoColor: {
|
||||||
if (typeof SettingsData === "undefined")
|
if (typeof SettingsData === "undefined")
|
||||||
return "";
|
return "";
|
||||||
const colorOverride = SettingsData.launcherLogoColorOverride;
|
const colorOverride = SettingsData.launcherLogoColorOverride;
|
||||||
@@ -1246,7 +1526,7 @@ Singleton {
|
|||||||
return colorOverride;
|
return colorOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
property string effectiveLogoColor: {
|
property var effectiveLogoColor: {
|
||||||
if (typeof SettingsData === "undefined")
|
if (typeof SettingsData === "undefined")
|
||||||
return "";
|
return "";
|
||||||
|
|
||||||
@@ -1453,4 +1733,303 @@ Singleton {
|
|||||||
root.switchTheme(defaultTheme, true, false);
|
root.switchTheme(defaultTheme, true, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Theme mode automation functions
|
||||||
|
function themeAutoBackendAvailable() {
|
||||||
|
return typeof DMSService !== "undefined" && DMSService.isConnected && Array.isArray(DMSService.capabilities) && DMSService.capabilities.includes("theme.auto");
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyThemeAutoState(state) {
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.config && state.config.mode && state.config.mode !== SessionData.themeModeAutoMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof SessionData !== "undefined" && state.nextTransition !== undefined) {
|
||||||
|
SessionData.themeModeNextTransition = state.nextTransition || "";
|
||||||
|
}
|
||||||
|
if (state.isLight !== undefined && root.isLightMode !== state.isLight) {
|
||||||
|
root.setLightMode(state.isLight, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncTimeThemeSchedule() {
|
||||||
|
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DMSService.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeModeActive = SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "time";
|
||||||
|
|
||||||
|
if (!timeModeActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DMSService.sendRequest("theme.auto.setMode", {
|
||||||
|
"mode": "time"
|
||||||
|
});
|
||||||
|
|
||||||
|
const shareSettings = SessionData.themeModeShareGammaSettings;
|
||||||
|
const startHour = shareSettings ? SessionData.nightModeStartHour : SessionData.themeModeStartHour;
|
||||||
|
const startMinute = shareSettings ? SessionData.nightModeStartMinute : SessionData.themeModeStartMinute;
|
||||||
|
const endHour = shareSettings ? SessionData.nightModeEndHour : SessionData.themeModeEndHour;
|
||||||
|
const endMinute = shareSettings ? SessionData.nightModeEndMinute : SessionData.themeModeEndMinute;
|
||||||
|
|
||||||
|
DMSService.sendRequest("theme.auto.setSchedule", {
|
||||||
|
"startHour": startHour,
|
||||||
|
"startMinute": startMinute,
|
||||||
|
"endHour": endHour,
|
||||||
|
"endMinute": endMinute
|
||||||
|
}, response => {
|
||||||
|
if (response && response.error) {
|
||||||
|
console.error("Theme automation: Failed to sync time schedule:", response.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
DMSService.sendRequest("theme.auto.setEnabled", {
|
||||||
|
"enabled": true
|
||||||
|
});
|
||||||
|
DMSService.sendRequest("theme.auto.trigger", {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncLocationThemeSchedule() {
|
||||||
|
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DMSService.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const locationModeActive = SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "location";
|
||||||
|
|
||||||
|
if (!locationModeActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DMSService.sendRequest("theme.auto.setMode", {
|
||||||
|
"mode": "location"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (SessionData.nightModeUseIPLocation) {
|
||||||
|
DMSService.sendRequest("theme.auto.setUseIPLocation", {
|
||||||
|
"use": true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
DMSService.sendRequest("theme.auto.setUseIPLocation", {
|
||||||
|
"use": false
|
||||||
|
});
|
||||||
|
if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||||
|
DMSService.sendRequest("theme.auto.setLocation", {
|
||||||
|
"latitude": SessionData.latitude,
|
||||||
|
"longitude": SessionData.longitude
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DMSService.sendRequest("theme.auto.setEnabled", {
|
||||||
|
"enabled": true
|
||||||
|
});
|
||||||
|
DMSService.sendRequest("theme.auto.trigger", {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateThemeMode() {
|
||||||
|
if (typeof SessionData === "undefined" || !SessionData.themeModeAutoEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeAutoBackendAvailable()) {
|
||||||
|
DMSService.sendRequest("theme.auto.getState", null, response => {
|
||||||
|
if (response && response.result) {
|
||||||
|
applyThemeAutoState(response.result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mode = SessionData.themeModeAutoMode;
|
||||||
|
|
||||||
|
if (mode === "location") {
|
||||||
|
evaluateLocationBasedThemeMode();
|
||||||
|
} else {
|
||||||
|
evaluateTimeBasedThemeMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateLocationBasedThemeMode() {
|
||||||
|
if (typeof DisplayService !== "undefined") {
|
||||||
|
const shouldBeLight = DisplayService.gammaIsDay;
|
||||||
|
if (root.isLightMode !== shouldBeLight) {
|
||||||
|
root.setLightMode(shouldBeLight, true, true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||||
|
const shouldBeLight = calculateIsDaytime(SessionData.latitude, SessionData.longitude);
|
||||||
|
if (root.isLightMode !== shouldBeLight) {
|
||||||
|
root.setLightMode(shouldBeLight, true, true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.themeModeAutomationActive) {
|
||||||
|
if (SessionData.nightModeUseIPLocation) {
|
||||||
|
console.warn("Theme automation: Waiting for IP location from backend");
|
||||||
|
} else {
|
||||||
|
console.warn("Theme automation: Location mode requires coordinates");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateTimeBasedThemeMode() {
|
||||||
|
const shareSettings = SessionData.themeModeShareGammaSettings;
|
||||||
|
|
||||||
|
const startHour = shareSettings ? SessionData.nightModeStartHour : SessionData.themeModeStartHour;
|
||||||
|
const startMinute = shareSettings ? SessionData.nightModeStartMinute : SessionData.themeModeStartMinute;
|
||||||
|
const endHour = shareSettings ? SessionData.nightModeEndHour : SessionData.themeModeEndHour;
|
||||||
|
const endMinute = shareSettings ? SessionData.nightModeEndMinute : SessionData.themeModeEndMinute;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
||||||
|
const startMinutes = startHour * 60 + startMinute;
|
||||||
|
const endMinutes = endHour * 60 + endMinute;
|
||||||
|
|
||||||
|
let shouldBeLight;
|
||||||
|
if (startMinutes < endMinutes) {
|
||||||
|
shouldBeLight = currentMinutes < startMinutes || currentMinutes >= endMinutes;
|
||||||
|
} else {
|
||||||
|
shouldBeLight = currentMinutes >= endMinutes && currentMinutes < startMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.isLightMode !== shouldBeLight) {
|
||||||
|
root.setLightMode(shouldBeLight, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateIsDaytime(lat, lng) {
|
||||||
|
const now = new Date();
|
||||||
|
const start = new Date(now.getFullYear(), 0, 0);
|
||||||
|
const diff = now - start;
|
||||||
|
const dayOfYear = Math.floor(diff / 86400000);
|
||||||
|
const latRad = lat * Math.PI / 180;
|
||||||
|
|
||||||
|
const declination = 23.45 * Math.sin((360 / 365) * (dayOfYear - 81) * Math.PI / 180);
|
||||||
|
const declinationRad = declination * Math.PI / 180;
|
||||||
|
|
||||||
|
const cosHourAngle = -Math.tan(latRad) * Math.tan(declinationRad);
|
||||||
|
|
||||||
|
if (cosHourAngle > 1) {
|
||||||
|
return false; // Polar night
|
||||||
|
}
|
||||||
|
if (cosHourAngle < -1) {
|
||||||
|
return true; // Midnight sun
|
||||||
|
}
|
||||||
|
|
||||||
|
const hourAngle = Math.acos(cosHourAngle);
|
||||||
|
const hourAngleDeg = hourAngle * 180 / Math.PI;
|
||||||
|
|
||||||
|
const sunriseHour = 12 - hourAngleDeg / 15;
|
||||||
|
const sunsetHour = 12 + hourAngleDeg / 15;
|
||||||
|
|
||||||
|
const timeZoneOffset = now.getTimezoneOffset() / 60;
|
||||||
|
const localSunrise = sunriseHour - lng / 15 - timeZoneOffset;
|
||||||
|
const localSunset = sunsetHour - lng / 15 - timeZoneOffset;
|
||||||
|
|
||||||
|
const currentHour = now.getHours() + now.getMinutes() / 60;
|
||||||
|
|
||||||
|
const normalizeSunrise = ((localSunrise % 24) + 24) % 24;
|
||||||
|
const normalizeSunset = ((localSunset % 24) + 24) % 24;
|
||||||
|
|
||||||
|
return currentHour >= normalizeSunrise && currentHour < normalizeSunset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to send location to backend
|
||||||
|
function sendLocationToBackend() {
|
||||||
|
if (typeof SessionData === "undefined" || typeof DMSService === "undefined") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DMSService.isConnected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SessionData.nightModeUseIPLocation) {
|
||||||
|
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||||
|
"use": true
|
||||||
|
}, response => {
|
||||||
|
if (response?.error) {
|
||||||
|
console.warn("Theme automation: Failed to enable IP location", response.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||||
|
DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
|
||||||
|
"use": false
|
||||||
|
}, response => {
|
||||||
|
if (!response.error) {
|
||||||
|
DMSService.sendRequest("wayland.gamma.setLocation", {
|
||||||
|
"latitude": SessionData.latitude,
|
||||||
|
"longitude": SessionData.longitude
|
||||||
|
}, locResp => {
|
||||||
|
if (locResp?.error) {
|
||||||
|
console.warn("Theme automation: Failed to set location", locResp.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: locationRetryTimer
|
||||||
|
interval: 1000
|
||||||
|
repeat: true
|
||||||
|
running: false
|
||||||
|
property int retryCount: 0
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (root.sendLocationToBackend()) {
|
||||||
|
stop();
|
||||||
|
retryCount = 0;
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
} else {
|
||||||
|
retryCount++;
|
||||||
|
if (retryCount >= 10) {
|
||||||
|
stop();
|
||||||
|
retryCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startThemeModeAutomation() {
|
||||||
|
root.themeModeAutomationActive = true;
|
||||||
|
|
||||||
|
root.syncTimeThemeSchedule();
|
||||||
|
root.syncLocationThemeSchedule();
|
||||||
|
|
||||||
|
const sent = root.sendLocationToBackend();
|
||||||
|
|
||||||
|
if (!sent && typeof SessionData !== "undefined" && SessionData.themeModeAutoMode === "location") {
|
||||||
|
locationRetryTimer.start();
|
||||||
|
} else {
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopThemeModeAutomation() {
|
||||||
|
root.themeModeAutomationActive = false;
|
||||||
|
if (typeof DMSService !== "undefined" && DMSService.isConnected) {
|
||||||
|
DMSService.sendRequest("theme.auto.setEnabled", {
|
||||||
|
"enabled": false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,14 @@ var SPEC = {
|
|||||||
nightModeUseIPLocation: { def: false },
|
nightModeUseIPLocation: { def: false },
|
||||||
nightModeLocationProvider: { def: "" },
|
nightModeLocationProvider: { def: "" },
|
||||||
|
|
||||||
|
themeModeAutoEnabled: { def: false },
|
||||||
|
themeModeAutoMode: { def: "time" },
|
||||||
|
themeModeStartHour: { def: 18 },
|
||||||
|
themeModeStartMinute: { def: 0 },
|
||||||
|
themeModeEndHour: { def: 6 },
|
||||||
|
themeModeEndMinute: { def: 0 },
|
||||||
|
themeModeShareGammaSettings: { def: true },
|
||||||
|
|
||||||
weatherLocation: { def: "New York, NY" },
|
weatherLocation: { def: "New York, NY" },
|
||||||
weatherCoordinates: { def: "40.7128,-74.0060" },
|
weatherCoordinates: { def: "40.7128,-74.0060" },
|
||||||
|
|
||||||
@@ -61,7 +69,9 @@ var SPEC = {
|
|||||||
|
|
||||||
hiddenApps: { def: [] },
|
hiddenApps: { def: [] },
|
||||||
appOverrides: { def: {} },
|
appOverrides: { def: {} },
|
||||||
searchAppActions: { def: true }
|
searchAppActions: { def: true },
|
||||||
|
|
||||||
|
vpnLastConnected: { def: "" }
|
||||||
};
|
};
|
||||||
|
|
||||||
function getValidKeys() {
|
function getValidKeys() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.pragma library
|
.pragma library
|
||||||
|
|
||||||
.import "./SessionSpec.js" as SpecModule
|
.import "./SessionSpec.js" as SpecModule
|
||||||
|
|
||||||
function parse(root, jsonObj) {
|
function parse(root, jsonObj) {
|
||||||
var SPEC = SpecModule.SPEC;
|
var SPEC = SpecModule.SPEC;
|
||||||
@@ -68,6 +68,11 @@ function migrateToVersion(obj, targetVersion, settingsData) {
|
|||||||
session.configVersion = 2;
|
session.configVersion = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentVersion < 3) {
|
||||||
|
console.info("SessionData: Migrating session to version 3");
|
||||||
|
session.configVersion = 3;
|
||||||
|
}
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ var SPEC = {
|
|||||||
|
|
||||||
widgetBackgroundColor: { def: "sch" },
|
widgetBackgroundColor: { def: "sch" },
|
||||||
widgetColorMode: { def: "default" },
|
widgetColorMode: { def: "default" },
|
||||||
|
controlCenterTileColorMode: { def: "primary" },
|
||||||
cornerRadius: { def: 12, onChange: "updateCompositorLayout" },
|
cornerRadius: { def: 12, onChange: "updateCompositorLayout" },
|
||||||
niriLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
|
niriLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
niriLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
niriLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
@@ -32,6 +33,7 @@ var SPEC = {
|
|||||||
|
|
||||||
use24HourClock: { def: true },
|
use24HourClock: { def: true },
|
||||||
showSeconds: { def: false },
|
showSeconds: { def: false },
|
||||||
|
padHours12Hour: { def: false },
|
||||||
useFahrenheit: { def: false },
|
useFahrenheit: { def: false },
|
||||||
windSpeedUnit: { def: "kmh" },
|
windSpeedUnit: { def: "kmh" },
|
||||||
nightModeEnabled: { def: false },
|
nightModeEnabled: { def: false },
|
||||||
@@ -78,16 +80,18 @@ var SPEC = {
|
|||||||
privacyShowCameraIcon: { def: false },
|
privacyShowCameraIcon: { def: false },
|
||||||
privacyShowScreenShareIcon: { def: false },
|
privacyShowScreenShareIcon: { def: false },
|
||||||
|
|
||||||
controlCenterWidgets: { def: [
|
controlCenterWidgets: {
|
||||||
{ id: "volumeSlider", enabled: true, width: 50 },
|
def: [
|
||||||
{ id: "brightnessSlider", enabled: true, width: 50 },
|
{ id: "volumeSlider", enabled: true, width: 50 },
|
||||||
{ id: "wifi", enabled: true, width: 50 },
|
{ id: "brightnessSlider", enabled: true, width: 50 },
|
||||||
{ id: "bluetooth", enabled: true, width: 50 },
|
{ id: "wifi", enabled: true, width: 50 },
|
||||||
{ id: "audioOutput", enabled: true, width: 50 },
|
{ id: "bluetooth", enabled: true, width: 50 },
|
||||||
{ id: "audioInput", enabled: true, width: 50 },
|
{ id: "audioOutput", enabled: true, width: 50 },
|
||||||
{ id: "nightMode", enabled: true, width: 50 },
|
{ id: "audioInput", enabled: true, width: 50 },
|
||||||
{ id: "darkMode", enabled: true, width: 50 }
|
{ id: "nightMode", enabled: true, width: 50 },
|
||||||
]},
|
{ id: "darkMode", enabled: true, width: 50 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
showWorkspaceIndex: { def: false },
|
showWorkspaceIndex: { def: false },
|
||||||
showWorkspaceName: { def: false },
|
showWorkspaceName: { def: false },
|
||||||
@@ -95,6 +99,7 @@ var SPEC = {
|
|||||||
workspaceScrolling: { def: false },
|
workspaceScrolling: { def: false },
|
||||||
showWorkspaceApps: { def: false },
|
showWorkspaceApps: { def: false },
|
||||||
maxWorkspaceIcons: { def: 3 },
|
maxWorkspaceIcons: { def: 3 },
|
||||||
|
workspaceAppIconSizeOffset: { def: 0 },
|
||||||
groupWorkspaceApps: { def: true },
|
groupWorkspaceApps: { def: true },
|
||||||
workspaceFollowFocus: { def: false },
|
workspaceFollowFocus: { def: false },
|
||||||
showOccupiedWorkspacesOnly: { def: false },
|
showOccupiedWorkspacesOnly: { def: false },
|
||||||
@@ -115,16 +120,21 @@ var SPEC = {
|
|||||||
clockCompactMode: { def: false },
|
clockCompactMode: { def: false },
|
||||||
focusedWindowCompactMode: { def: false },
|
focusedWindowCompactMode: { def: false },
|
||||||
runningAppsCompactMode: { def: true },
|
runningAppsCompactMode: { def: true },
|
||||||
|
barMaxVisibleApps: { def: 0 },
|
||||||
|
barMaxVisibleRunningApps: { def: 0 },
|
||||||
|
barShowOverflowBadge: { def: true },
|
||||||
keyboardLayoutNameCompactMode: { def: false },
|
keyboardLayoutNameCompactMode: { def: false },
|
||||||
runningAppsCurrentWorkspace: { def: false },
|
runningAppsCurrentWorkspace: { def: false },
|
||||||
runningAppsGroupByApp: { def: false },
|
runningAppsGroupByApp: { def: false },
|
||||||
appIdSubstitutions: { def: [
|
appIdSubstitutions: {
|
||||||
{ pattern: "Spotify", replacement: "spotify", type: "exact" },
|
def: [
|
||||||
{ pattern: "beepertexts", replacement: "beeper", type: "exact" },
|
{ pattern: "Spotify", replacement: "spotify", type: "exact" },
|
||||||
{ pattern: "home assistant desktop", replacement: "homeassistant-desktop", type: "exact" },
|
{ pattern: "beepertexts", replacement: "beeper", type: "exact" },
|
||||||
{ pattern: "com.transmissionbt.transmission", replacement: "transmission-gtk", type: "contains" },
|
{ pattern: "home assistant desktop", replacement: "homeassistant-desktop", type: "exact" },
|
||||||
{ pattern: "^steam_app_(\\d+)$", replacement: "steam_icon_$1", type: "regex" }
|
{ 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: "" },
|
||||||
@@ -132,6 +142,10 @@ var SPEC = {
|
|||||||
|
|
||||||
appLauncherViewMode: { def: "list" },
|
appLauncherViewMode: { def: "list" },
|
||||||
spotlightModalViewMode: { def: "list" },
|
spotlightModalViewMode: { def: "list" },
|
||||||
|
browserPickerViewMode: { def: "grid" },
|
||||||
|
browserUsageHistory: { def: {} },
|
||||||
|
appPickerViewMode: { def: "grid" },
|
||||||
|
filePickerUsageHistory: { def: {} },
|
||||||
sortAppsAlphabetically: { def: false },
|
sortAppsAlphabetically: { def: false },
|
||||||
appLauncherGridColumns: { def: 4 },
|
appLauncherGridColumns: { def: 4 },
|
||||||
spotlightCloseNiriOverview: { def: true },
|
spotlightCloseNiriOverview: { def: true },
|
||||||
@@ -148,7 +162,6 @@ var SPEC = {
|
|||||||
weatherEnabled: { def: true },
|
weatherEnabled: { def: true },
|
||||||
|
|
||||||
networkPreference: { def: "auto" },
|
networkPreference: { def: "auto" },
|
||||||
vpnLastConnected: { def: "" },
|
|
||||||
|
|
||||||
iconTheme: { def: "System Default", onChange: "applyStoredIconTheme" },
|
iconTheme: { def: "System Default", onChange: "applyStoredIconTheme" },
|
||||||
availableIconThemes: { def: ["System Default"], persist: false },
|
availableIconThemes: { def: ["System Default"], persist: false },
|
||||||
@@ -262,6 +275,9 @@ var SPEC = {
|
|||||||
dockLauncherLogoSizeOffset: { def: 0 },
|
dockLauncherLogoSizeOffset: { def: 0 },
|
||||||
dockLauncherLogoBrightness: { def: 0.5, coerce: percentToUnit },
|
dockLauncherLogoBrightness: { def: 0.5, coerce: percentToUnit },
|
||||||
dockLauncherLogoContrast: { def: 1, coerce: percentToUnit },
|
dockLauncherLogoContrast: { def: 1, coerce: percentToUnit },
|
||||||
|
dockMaxVisibleApps: { def: 0 },
|
||||||
|
dockMaxVisibleRunningApps: { def: 0 },
|
||||||
|
dockShowOverflowBadge: { def: true },
|
||||||
|
|
||||||
notificationOverlayEnabled: { def: false },
|
notificationOverlayEnabled: { def: false },
|
||||||
overviewRows: { def: 2, persist: false },
|
overviewRows: { def: 2, persist: false },
|
||||||
@@ -276,6 +292,7 @@ var SPEC = {
|
|||||||
lockScreenShowDate: { def: true },
|
lockScreenShowDate: { def: true },
|
||||||
lockScreenShowProfileImage: { def: true },
|
lockScreenShowProfileImage: { def: true },
|
||||||
lockScreenShowPasswordField: { def: true },
|
lockScreenShowPasswordField: { def: true },
|
||||||
|
lockScreenShowMediaPlayer: { def: true },
|
||||||
lockScreenPowerOffMonitorsOnLock: { def: false },
|
lockScreenPowerOffMonitorsOnLock: { def: false },
|
||||||
enableFprint: { def: false },
|
enableFprint: { def: false },
|
||||||
maxFprintTries: { def: 15 },
|
maxFprintTries: { def: 15 },
|
||||||
@@ -300,7 +317,7 @@ var SPEC = {
|
|||||||
osdAlwaysShowValue: { def: false },
|
osdAlwaysShowValue: { def: false },
|
||||||
osdPosition: { def: 5 },
|
osdPosition: { def: 5 },
|
||||||
osdVolumeEnabled: { def: true },
|
osdVolumeEnabled: { def: true },
|
||||||
osdMediaVolumeEnabled : { def: true },
|
osdMediaVolumeEnabled: { def: true },
|
||||||
osdBrightnessEnabled: { def: true },
|
osdBrightnessEnabled: { def: true },
|
||||||
osdIdleInhibitorEnabled: { def: true },
|
osdIdleInhibitorEnabled: { def: true },
|
||||||
osdMicMuteEnabled: { def: true },
|
osdMicMuteEnabled: { def: true },
|
||||||
@@ -330,53 +347,60 @@ var SPEC = {
|
|||||||
showOnLastDisplay: { def: {} },
|
showOnLastDisplay: { def: {} },
|
||||||
niriOutputSettings: { def: {} },
|
niriOutputSettings: { def: {} },
|
||||||
hyprlandOutputSettings: { def: {} },
|
hyprlandOutputSettings: { def: {} },
|
||||||
|
displayProfiles: { def: {} },
|
||||||
|
activeDisplayProfile: { def: {} },
|
||||||
|
displayProfileAutoSelect: { def: false },
|
||||||
|
displayShowDisconnected: { def: false },
|
||||||
|
displaySnapToEdge: { def: true },
|
||||||
|
|
||||||
barConfigs: { def: [{
|
barConfigs: {
|
||||||
id: "default",
|
def: [{
|
||||||
name: "Main Bar",
|
id: "default",
|
||||||
enabled: true,
|
name: "Main Bar",
|
||||||
position: 0,
|
enabled: true,
|
||||||
screenPreferences: ["all"],
|
position: 0,
|
||||||
showOnLastDisplay: true,
|
screenPreferences: ["all"],
|
||||||
leftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"],
|
showOnLastDisplay: true,
|
||||||
centerWidgets: ["music", "clock", "weather"],
|
leftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"],
|
||||||
rightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"],
|
centerWidgets: ["music", "clock", "weather"],
|
||||||
spacing: 4,
|
rightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"],
|
||||||
innerPadding: 4,
|
spacing: 4,
|
||||||
bottomGap: 0,
|
innerPadding: 4,
|
||||||
transparency: 1.0,
|
bottomGap: 0,
|
||||||
widgetTransparency: 1.0,
|
transparency: 1.0,
|
||||||
squareCorners: false,
|
widgetTransparency: 1.0,
|
||||||
noBackground: false,
|
squareCorners: false,
|
||||||
gothCornersEnabled: false,
|
noBackground: false,
|
||||||
gothCornerRadiusOverride: false,
|
gothCornersEnabled: false,
|
||||||
gothCornerRadiusValue: 12,
|
gothCornerRadiusOverride: false,
|
||||||
borderEnabled: false,
|
gothCornerRadiusValue: 12,
|
||||||
borderColor: "surfaceText",
|
borderEnabled: false,
|
||||||
borderOpacity: 1.0,
|
borderColor: "surfaceText",
|
||||||
borderThickness: 1,
|
borderOpacity: 1.0,
|
||||||
widgetOutlineEnabled: false,
|
borderThickness: 1,
|
||||||
widgetOutlineColor: "primary",
|
widgetOutlineEnabled: false,
|
||||||
widgetOutlineOpacity: 1.0,
|
widgetOutlineColor: "primary",
|
||||||
widgetOutlineThickness: 1,
|
widgetOutlineOpacity: 1.0,
|
||||||
fontScale: 1.0,
|
widgetOutlineThickness: 1,
|
||||||
autoHide: false,
|
fontScale: 1.0,
|
||||||
autoHideDelay: 250,
|
autoHide: false,
|
||||||
showOnWindowsOpen: false,
|
autoHideDelay: 250,
|
||||||
openOnOverview: false,
|
showOnWindowsOpen: false,
|
||||||
visible: true,
|
openOnOverview: false,
|
||||||
popupGapsAuto: true,
|
visible: true,
|
||||||
popupGapsManual: 4,
|
popupGapsAuto: true,
|
||||||
maximizeDetection: true,
|
popupGapsManual: 4,
|
||||||
scrollEnabled: true,
|
maximizeDetection: true,
|
||||||
scrollXBehavior: "column",
|
scrollEnabled: true,
|
||||||
scrollYBehavior: "workspace",
|
scrollXBehavior: "column",
|
||||||
shadowIntensity: 0,
|
scrollYBehavior: "workspace",
|
||||||
shadowOpacity: 60,
|
shadowIntensity: 0,
|
||||||
shadowColorMode: "text",
|
shadowOpacity: 60,
|
||||||
shadowCustomColor: "#000000",
|
shadowColorMode: "text",
|
||||||
clickThrough: false
|
shadowCustomColor: "#000000",
|
||||||
}], onChange: "updateBarConfigs" },
|
clickThrough: false
|
||||||
|
}], onChange: "updateBarConfigs"
|
||||||
|
},
|
||||||
|
|
||||||
desktopClockEnabled: { def: false },
|
desktopClockEnabled: { def: false },
|
||||||
desktopClockStyle: { def: "analog" },
|
desktopClockStyle: { def: "analog" },
|
||||||
@@ -431,7 +455,7 @@ var SPEC = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function getValidKeys() {
|
function getValidKeys() {
|
||||||
return Object.keys(SPEC).filter(function(k) { return SPEC[k].persist !== false; }).concat(["configVersion"]);
|
return Object.keys(SPEC).filter(function (k) { return SPEC[k].persist !== false; }).concat(["configVersion"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function set(root, key, value, saveFn, hooks) {
|
function set(root, key, value, saveFn, hooks) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.pragma library
|
.pragma library
|
||||||
|
|
||||||
.import "./SettingsSpec.js" as SpecModule
|
.import "./SettingsSpec.js" as SpecModule
|
||||||
|
|
||||||
function parse(root, jsonObj) {
|
function parse(root, jsonObj) {
|
||||||
var SPEC = SpecModule.SPEC;
|
var SPEC = SpecModule.SPEC;
|
||||||
|
|||||||
@@ -550,6 +550,11 @@ Item {
|
|||||||
AppPickerModal {
|
AppPickerModal {
|
||||||
id: filePickerModal
|
id: filePickerModal
|
||||||
title: I18n.tr("Open with...")
|
title: I18n.tr("Open with...")
|
||||||
|
viewMode: SettingsData.appPickerViewMode || "grid"
|
||||||
|
|
||||||
|
onViewModeChanged: {
|
||||||
|
SettingsData.set("appPickerViewMode", viewMode)
|
||||||
|
}
|
||||||
|
|
||||||
function shellEscape(str) {
|
function shellEscape(str) {
|
||||||
return "'" + str.replace(/'/g, "'\\''") + "'";
|
return "'" + str.replace(/'/g, "'\\''") + "'";
|
||||||
@@ -644,6 +649,30 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: workspaceRenameModalLoader
|
||||||
|
|
||||||
|
active: false
|
||||||
|
|
||||||
|
Component.onCompleted: PopoutService.workspaceRenameModalLoader = workspaceRenameModalLoader
|
||||||
|
|
||||||
|
WorkspaceRenameModal {
|
||||||
|
id: workspaceRenameModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: windowRuleModalLoader
|
||||||
|
|
||||||
|
active: false
|
||||||
|
|
||||||
|
Component.onCompleted: PopoutService.windowRuleModalLoader = windowRuleModalLoader
|
||||||
|
|
||||||
|
WindowRuleModal {
|
||||||
|
id: windowRuleModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LazyLoader {
|
LazyLoader {
|
||||||
id: processListModalLoader
|
id: processListModalLoader
|
||||||
|
|
||||||
@@ -769,6 +798,8 @@ Item {
|
|||||||
hyprKeybindsModalLoader: hyprKeybindsModalLoader
|
hyprKeybindsModalLoader: hyprKeybindsModalLoader
|
||||||
dankBarRepeater: dankBarRepeater
|
dankBarRepeater: dankBarRepeater
|
||||||
hyprlandOverviewLoader: hyprlandOverviewLoader
|
hyprlandOverviewLoader: hyprlandOverviewLoader
|
||||||
|
workspaceRenameModalLoader: workspaceRenameModalLoader
|
||||||
|
windowRuleModalLoader: windowRuleModalLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
|
import Quickshell.Wayland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
import qs.Modules.Settings.DisplayConfig
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -15,6 +17,8 @@ Item {
|
|||||||
required property var hyprKeybindsModalLoader
|
required property var hyprKeybindsModalLoader
|
||||||
required property var dankBarRepeater
|
required property var dankBarRepeater
|
||||||
required property var hyprlandOverviewLoader
|
required property var hyprlandOverviewLoader
|
||||||
|
required property var workspaceRenameModalLoader
|
||||||
|
required property var windowRuleModalLoader
|
||||||
|
|
||||||
function getFirstBar() {
|
function getFirstBar() {
|
||||||
if (!root.dankBarRepeater || root.dankBarRepeater.count === 0)
|
if (!root.dankBarRepeater || root.dankBarRepeater.count === 0)
|
||||||
@@ -1062,7 +1066,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleQuery(query: string): string {
|
function toggleQuery(query: string): string {
|
||||||
PopoutService.toggleDankLauncherV2();
|
PopoutService.toggleDankLauncherV2WithQuery(query);
|
||||||
return "LAUNCHER_TOGGLE_QUERY_SUCCESS";
|
return "LAUNCHER_TOGGLE_QUERY_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1106,13 +1110,86 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleQuery(query: string): string {
|
function toggleQuery(query: string): string {
|
||||||
PopoutService.toggleDankLauncherV2();
|
PopoutService.toggleDankLauncherV2WithQuery(query);
|
||||||
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS";
|
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
target: "spotlight"
|
target: "spotlight"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function info(message: string): string {
|
||||||
|
if (!message)
|
||||||
|
return "ERROR: No message specified";
|
||||||
|
|
||||||
|
ToastService.showInfo(message);
|
||||||
|
return "TOAST_INFO_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function infoWith(message: string, details: string, command: string, category: string): string {
|
||||||
|
if (!message)
|
||||||
|
return "ERROR: No message specified";
|
||||||
|
|
||||||
|
ToastService.showInfo(message, details, command, category);
|
||||||
|
return "TOAST_INFO_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function warn(message: string): string {
|
||||||
|
if (!message)
|
||||||
|
return "ERROR: No message specified";
|
||||||
|
|
||||||
|
ToastService.showWarning(message);
|
||||||
|
return "TOAST_WARN_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function warnWith(message: string, details: string, command: string, category: string): string {
|
||||||
|
if (!message)
|
||||||
|
return "ERROR: No message specified";
|
||||||
|
|
||||||
|
ToastService.showWarning(message, details, command, category);
|
||||||
|
return "TOAST_WARN_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(message: string): string {
|
||||||
|
if (!message)
|
||||||
|
return "ERROR: No message specified";
|
||||||
|
|
||||||
|
ToastService.showError(message);
|
||||||
|
return "TOAST_ERROR_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorWith(message: string, details: string, command: string, category: string): string {
|
||||||
|
if (!message)
|
||||||
|
return "ERROR: No message specified";
|
||||||
|
|
||||||
|
ToastService.showError(message, details, command, category);
|
||||||
|
return "TOAST_ERROR_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide(): string {
|
||||||
|
ToastService.hideToast();
|
||||||
|
return "TOAST_HIDE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismiss(category: string): string {
|
||||||
|
if (!category)
|
||||||
|
return "ERROR: No category specified";
|
||||||
|
|
||||||
|
ToastService.dismissCategory(category);
|
||||||
|
return "TOAST_DISMISS_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function status(): string {
|
||||||
|
if (!ToastService.toastVisible)
|
||||||
|
return "hidden";
|
||||||
|
|
||||||
|
const levels = ["info", "warn", "error"];
|
||||||
|
return `visible:${levels[ToastService.currentLevel]}:${ToastService.currentMessage}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "toast"
|
||||||
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function open(): string {
|
function open(): string {
|
||||||
FirstLaunchService.showWelcome();
|
FirstLaunchService.showWelcome();
|
||||||
@@ -1292,4 +1369,165 @@ Item {
|
|||||||
|
|
||||||
target: "desktopWidget"
|
target: "desktopWidget"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
root.workspaceRenameModalLoader.active = true;
|
||||||
|
if (root.workspaceRenameModalLoader.item) {
|
||||||
|
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
|
||||||
|
root.workspaceRenameModalLoader.item.show(ws?.name || "");
|
||||||
|
return "WORKSPACE_RENAME_MODAL_OPENED";
|
||||||
|
}
|
||||||
|
return "WORKSPACE_RENAME_MODAL_NOT_FOUND";
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
if (root.workspaceRenameModalLoader.item) {
|
||||||
|
root.workspaceRenameModalLoader.item.hide();
|
||||||
|
return "WORKSPACE_RENAME_MODAL_CLOSED";
|
||||||
|
}
|
||||||
|
return "WORKSPACE_RENAME_MODAL_NOT_FOUND";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
root.workspaceRenameModalLoader.active = true;
|
||||||
|
if (root.workspaceRenameModalLoader.item) {
|
||||||
|
if (root.workspaceRenameModalLoader.item.visible) {
|
||||||
|
root.workspaceRenameModalLoader.item.hide();
|
||||||
|
return "WORKSPACE_RENAME_MODAL_CLOSED";
|
||||||
|
}
|
||||||
|
const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
|
||||||
|
root.workspaceRenameModalLoader.item.show(ws?.name || "");
|
||||||
|
return "WORKSPACE_RENAME_MODAL_OPENED";
|
||||||
|
}
|
||||||
|
return "WORKSPACE_RENAME_MODAL_NOT_FOUND";
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "workspace-rename"
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function getFocusedWindow() {
|
||||||
|
const active = ToplevelManager.activeToplevel;
|
||||||
|
if (!active)
|
||||||
|
return null;
|
||||||
|
return {
|
||||||
|
appId: active.appId || "",
|
||||||
|
title: active.title || ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(): string {
|
||||||
|
if (!CompositorService.isNiri)
|
||||||
|
return "WINDOW_RULES_NIRI_ONLY";
|
||||||
|
root.windowRuleModalLoader.active = true;
|
||||||
|
if (root.windowRuleModalLoader.item) {
|
||||||
|
root.windowRuleModalLoader.item.show(getFocusedWindow());
|
||||||
|
return "WINDOW_RULE_MODAL_OPENED";
|
||||||
|
}
|
||||||
|
return "WINDOW_RULE_MODAL_NOT_FOUND";
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
if (root.windowRuleModalLoader.item) {
|
||||||
|
root.windowRuleModalLoader.item.hide();
|
||||||
|
return "WINDOW_RULE_MODAL_CLOSED";
|
||||||
|
}
|
||||||
|
return "WINDOW_RULE_MODAL_NOT_FOUND";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
if (!CompositorService.isNiri)
|
||||||
|
return "WINDOW_RULES_NIRI_ONLY";
|
||||||
|
root.windowRuleModalLoader.active = true;
|
||||||
|
if (root.windowRuleModalLoader.item) {
|
||||||
|
if (root.windowRuleModalLoader.item.visible) {
|
||||||
|
root.windowRuleModalLoader.item.hide();
|
||||||
|
return "WINDOW_RULE_MODAL_CLOSED";
|
||||||
|
}
|
||||||
|
root.windowRuleModalLoader.item.show(getFocusedWindow());
|
||||||
|
return "WINDOW_RULE_MODAL_OPENED";
|
||||||
|
}
|
||||||
|
return "WINDOW_RULE_MODAL_NOT_FOUND";
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "window-rules"
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function listProfiles(): string {
|
||||||
|
const profiles = DisplayConfigState.validatedProfiles;
|
||||||
|
const activeId = SettingsData.getActiveDisplayProfile(CompositorService.compositor);
|
||||||
|
const matchedId = DisplayConfigState.matchedProfile;
|
||||||
|
const lines = [];
|
||||||
|
|
||||||
|
for (const id in profiles) {
|
||||||
|
const p = profiles[id];
|
||||||
|
const flags = [];
|
||||||
|
if (id === activeId)
|
||||||
|
flags.push("active");
|
||||||
|
if (id === matchedId)
|
||||||
|
flags.push("matched");
|
||||||
|
const flagStr = flags.length > 0 ? " [" + flags.join(",") + "]" : "";
|
||||||
|
lines.push(p.name + flagStr + " -> " + JSON.stringify(p.outputSet));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines.length === 0)
|
||||||
|
return "No profiles configured";
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProfile(profileName: string): string {
|
||||||
|
if (!profileName)
|
||||||
|
return "ERROR: No profile name specified";
|
||||||
|
|
||||||
|
if (SettingsData.displayProfileAutoSelect)
|
||||||
|
return "ERROR: Auto profile selection is enabled. Use toggleAuto first";
|
||||||
|
|
||||||
|
const profiles = DisplayConfigState.validatedProfiles;
|
||||||
|
let profileId = null;
|
||||||
|
|
||||||
|
for (const id in profiles) {
|
||||||
|
if (profiles[id].name === profileName) {
|
||||||
|
profileId = id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profileId)
|
||||||
|
return `ERROR: Profile not found: ${profileName}`;
|
||||||
|
|
||||||
|
DisplayConfigState.activateProfile(profileId);
|
||||||
|
return `PROFILE_SET_SUCCESS: ${profileName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ! TODO - auto profile switching is buggy on niri and other compositors
|
||||||
|
function toggleAuto(): string {
|
||||||
|
return "ERROR: Auto profile selection is temporarily disabled due to compositor bugs";
|
||||||
|
}
|
||||||
|
|
||||||
|
function status(): string {
|
||||||
|
const auto = "off"; // disabled for now
|
||||||
|
const activeId = SettingsData.getActiveDisplayProfile(CompositorService.compositor);
|
||||||
|
const matchedId = DisplayConfigState.matchedProfile;
|
||||||
|
const profiles = DisplayConfigState.validatedProfiles;
|
||||||
|
const activeName = profiles[activeId]?.name || "none";
|
||||||
|
const matchedName = profiles[matchedId]?.name || "none";
|
||||||
|
const currentOutputs = JSON.stringify(DisplayConfigState.currentOutputSet);
|
||||||
|
|
||||||
|
return `auto: ${auto}\nactive: ${activeName}\nmatched: ${matchedName}\noutputs: ${currentOutputs}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function current(): string {
|
||||||
|
return JSON.stringify(DisplayConfigState.currentOutputSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh(): string {
|
||||||
|
DisplayConfigState.currentOutputSet = DisplayConfigState.buildCurrentOutputSet();
|
||||||
|
DisplayConfigState.validateProfiles();
|
||||||
|
return "Refreshed output state";
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "outputs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ Item {
|
|||||||
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
|
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
|
||||||
onTabChanged: tabName => modal.activeTab = tabName
|
onTabChanged: tabName => modal.activeTab = tabName
|
||||||
onClearAllClicked: {
|
onClearAllClicked: {
|
||||||
clearConfirmDialog.show(I18n.tr("Clear All History?"), I18n.tr("This will permanently delete all clipboard history."), function () {
|
const hasPinned = modal.pinnedCount > 0;
|
||||||
|
const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(modal.pinnedCount) : I18n.tr("This will permanently delete all clipboard history.");
|
||||||
|
clearConfirmDialog.show(I18n.tr("Clear History?"), message, function () {
|
||||||
modal.clearAll();
|
modal.clearAll();
|
||||||
modal.hide();
|
modal.hide();
|
||||||
}, function () {});
|
}, function () {});
|
||||||
|
|||||||
@@ -239,20 +239,16 @@ DankModal {
|
|||||||
|
|
||||||
function clearAll() {
|
function clearAll() {
|
||||||
const hasPinned = pinnedCount > 0;
|
const hasPinned = pinnedCount > 0;
|
||||||
const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(pinnedCount) : I18n.tr("This will permanently delete all clipboard history.");
|
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
|
||||||
|
if (response.error) {
|
||||||
clearConfirmDialog.show(I18n.tr("Clear History?"), message, function () {
|
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
|
||||||
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
|
return;
|
||||||
if (response.error) {
|
}
|
||||||
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
|
refreshClipboard();
|
||||||
return;
|
if (hasPinned) {
|
||||||
}
|
ToastService.showInfo(I18n.tr("History cleared. %1 pinned entries kept.").arg(pinnedCount));
|
||||||
refreshClipboard();
|
}
|
||||||
if (hasPinned) {
|
});
|
||||||
ToastService.showInfo(I18n.tr("History cleared. %1 pinned entries kept.").arg(pinnedCount));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, function () {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEntryPreview(entry) {
|
function getEntryPreview(entry) {
|
||||||
@@ -288,6 +284,20 @@ DankModal {
|
|||||||
modal: clipboardHistoryModal
|
modal: clipboardHistoryModal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: DMSService
|
||||||
|
function onClipboardStateUpdate(data) {
|
||||||
|
if (!clipboardHistoryModal.shouldBeVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newHistory = data.history || [];
|
||||||
|
internalEntries = newHistory;
|
||||||
|
pinnedEntries = newHistory.filter(e => e.pinned);
|
||||||
|
pinnedCount = pinnedEntries.length;
|
||||||
|
updateFilteredModel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ConfirmModal {
|
ConfirmModal {
|
||||||
id: clearConfirmDialog
|
id: clearConfirmDialog
|
||||||
confirmButtonText: I18n.tr("Clear All")
|
confirmButtonText: I18n.tr("Clear All")
|
||||||
@@ -295,13 +305,18 @@ DankModal {
|
|||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
clipboardHistoryModal.shouldHaveFocus = false;
|
clipboardHistoryModal.shouldHaveFocus = false;
|
||||||
} else if (clipboardHistoryModal.shouldBeVisible) {
|
return;
|
||||||
|
}
|
||||||
|
Qt.callLater(function () {
|
||||||
|
if (!clipboardHistoryModal.shouldBeVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
clipboardHistoryModal.shouldHaveFocus = true;
|
clipboardHistoryModal.shouldHaveFocus = true;
|
||||||
clipboardHistoryModal.modalFocusScope.forceActiveFocus();
|
clipboardHistoryModal.modalFocusScope.forceActiveFocus();
|
||||||
if (clipboardHistoryModal.contentLoader.item?.searchField) {
|
if (clipboardHistoryModal.contentLoader.item?.searchField) {
|
||||||
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus();
|
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus();
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (entryType !== "image") {
|
if (entryType !== "image" || listView.height <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,22 +93,41 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: visibilityTimer
|
||||||
|
interval: 100
|
||||||
|
onTriggered: thumbnailImage.checkVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkVisibility() {
|
||||||
|
if (entryType !== "image" || listView.height <= 0 || isVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing);
|
||||||
|
const viewTop = listView.contentY - ClipboardConstants.viewportBuffer;
|
||||||
|
const viewBottom = viewTop + listView.height + ClipboardConstants.extendedBuffer;
|
||||||
|
const nowVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom);
|
||||||
|
if (nowVisible) {
|
||||||
|
isVisible = true;
|
||||||
|
tryLoadImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: listView
|
target: listView
|
||||||
|
|
||||||
function onContentYChanged() {
|
function onContentYChanged() {
|
||||||
if (entryType !== "image") {
|
if (thumbnailImage.isVisible || entryType !== "image") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
visibilityTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing);
|
function onHeightChanged() {
|
||||||
const viewTop = listView.contentY - ClipboardConstants.viewportBuffer;
|
if (thumbnailImage.isVisible || entryType !== "image") {
|
||||||
const viewBottom = viewTop + listView.height + ClipboardConstants.extendedBuffer;
|
return;
|
||||||
const nowVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom);
|
|
||||||
|
|
||||||
if (nowVisible && !thumbnailImage.isVisible) {
|
|
||||||
thumbnailImage.isVisible = true;
|
|
||||||
thumbnailImage.tryLoadImage();
|
|
||||||
}
|
}
|
||||||
|
visibilityTimer.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ Rectangle {
|
|||||||
result.push(selectedItem.primaryAction);
|
result.push(selectedItem.primaryAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedItem?.type === "plugin") {
|
switch (selectedItem?.type) {
|
||||||
|
case "plugin":
|
||||||
var pluginActions = getPluginContextMenuActions();
|
var pluginActions = getPluginContextMenuActions();
|
||||||
for (var i = 0; i < pluginActions.length; i++) {
|
for (var i = 0; i < pluginActions.length; i++) {
|
||||||
var act = pluginActions[i];
|
var act = pluginActions[i];
|
||||||
@@ -44,24 +45,45 @@ Rectangle {
|
|||||||
pluginAction: act.action
|
pluginAction: act.action
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (selectedItem?.type === "app" && !selectedItem?.isCore) {
|
break;
|
||||||
|
case "plugin_browse":
|
||||||
if (selectedItem?.actions) {
|
if (selectedItem?.actions) {
|
||||||
for (var i = 0; i < selectedItem.actions.length; i++) {
|
for (var i = 0; i < selectedItem.actions.length; i++) {
|
||||||
result.push(selectedItem.actions[i]);
|
result.push(selectedItem.actions[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
case "app":
|
||||||
|
if (selectedItem?.isCore)
|
||||||
|
break;
|
||||||
|
if (selectedItem?.actions) {
|
||||||
|
for (var i = 0; i < selectedItem.actions.length; i++) {
|
||||||
|
result.push(selectedItem.actions[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (SessionService.nvidiaCommand) {
|
||||||
|
result.push({
|
||||||
|
name: I18n.tr("Launch on dGPU"),
|
||||||
|
icon: "memory",
|
||||||
|
action: "launch_dgpu"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool hasActions: {
|
readonly property bool hasActions: {
|
||||||
if (selectedItem?.type === "app" && !selectedItem?.isCore)
|
switch (selectedItem?.type) {
|
||||||
return true;
|
case "app":
|
||||||
if (selectedItem?.type === "plugin") {
|
return !selectedItem?.isCore;
|
||||||
var pluginActions = getPluginContextMenuActions();
|
case "plugin":
|
||||||
return pluginActions.length > 0;
|
return getPluginContextMenuActions().length > 0;
|
||||||
|
case "plugin_browse":
|
||||||
|
return selectedItem?.actions?.length > 0;
|
||||||
|
default:
|
||||||
|
return actions.length > 1;
|
||||||
}
|
}
|
||||||
return actions.length > 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
width: parent?.width ?? 200
|
width: parent?.width ?? 200
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
157
quickshell/Modals/DankLauncherV2/ControllerUtils.js
Normal file
157
quickshell/Modals/DankLauncherV2/ControllerUtils.js
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
.pragma library
|
||||||
|
|
||||||
|
function getFileIcon(filename) {
|
||||||
|
var ext = filename.lastIndexOf(".") > 0 ? filename.substring(filename.lastIndexOf(".") + 1).toLowerCase() : "";
|
||||||
|
|
||||||
|
switch (ext) {
|
||||||
|
case "pdf":
|
||||||
|
return "picture_as_pdf";
|
||||||
|
case "doc":
|
||||||
|
case "docx":
|
||||||
|
case "odt":
|
||||||
|
return "description";
|
||||||
|
case "xls":
|
||||||
|
case "xlsx":
|
||||||
|
case "ods":
|
||||||
|
return "table_chart";
|
||||||
|
case "ppt":
|
||||||
|
case "pptx":
|
||||||
|
case "odp":
|
||||||
|
return "slideshow";
|
||||||
|
case "txt":
|
||||||
|
case "md":
|
||||||
|
case "rst":
|
||||||
|
return "article";
|
||||||
|
case "jpg":
|
||||||
|
case "jpeg":
|
||||||
|
case "png":
|
||||||
|
case "gif":
|
||||||
|
case "svg":
|
||||||
|
case "webp":
|
||||||
|
return "image";
|
||||||
|
case "mp3":
|
||||||
|
case "wav":
|
||||||
|
case "flac":
|
||||||
|
case "ogg":
|
||||||
|
return "audio_file";
|
||||||
|
case "mp4":
|
||||||
|
case "mkv":
|
||||||
|
case "avi":
|
||||||
|
case "webm":
|
||||||
|
return "video_file";
|
||||||
|
case "zip":
|
||||||
|
case "tar":
|
||||||
|
case "gz":
|
||||||
|
case "7z":
|
||||||
|
case "rar":
|
||||||
|
return "folder_zip";
|
||||||
|
case "js":
|
||||||
|
case "ts":
|
||||||
|
case "py":
|
||||||
|
case "rs":
|
||||||
|
case "go":
|
||||||
|
case "java":
|
||||||
|
case "c":
|
||||||
|
case "cpp":
|
||||||
|
case "h":
|
||||||
|
return "code";
|
||||||
|
case "html":
|
||||||
|
case "css":
|
||||||
|
case "htm":
|
||||||
|
return "web";
|
||||||
|
case "json":
|
||||||
|
case "xml":
|
||||||
|
case "yaml":
|
||||||
|
case "yml":
|
||||||
|
return "data_object";
|
||||||
|
case "sh":
|
||||||
|
case "bash":
|
||||||
|
case "zsh":
|
||||||
|
return "terminal";
|
||||||
|
default:
|
||||||
|
return "insert_drive_file";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripIconPrefix(iconName) {
|
||||||
|
if (!iconName)
|
||||||
|
return "extension";
|
||||||
|
if (iconName.startsWith("unicode:"))
|
||||||
|
return iconName.substring(8);
|
||||||
|
if (iconName.startsWith("material:"))
|
||||||
|
return iconName.substring(9);
|
||||||
|
if (iconName.startsWith("image:"))
|
||||||
|
return iconName.substring(6);
|
||||||
|
return iconName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectIconType(iconName) {
|
||||||
|
if (!iconName)
|
||||||
|
return "material";
|
||||||
|
if (iconName.startsWith("unicode:"))
|
||||||
|
return "unicode";
|
||||||
|
if (iconName.startsWith("material:"))
|
||||||
|
return "material";
|
||||||
|
if (iconName.startsWith("image:"))
|
||||||
|
return "image";
|
||||||
|
if (iconName.indexOf("/") >= 0 || iconName.indexOf(".") >= 0)
|
||||||
|
return "image";
|
||||||
|
if (/^[a-z]+-[a-z]/.test(iconName.toLowerCase()))
|
||||||
|
return "image";
|
||||||
|
return "material";
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateCalculator(query) {
|
||||||
|
if (!query || query.length === 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var mathExpr = query.replace(/[^0-9+\-*/().%\s^]/g, "");
|
||||||
|
if (mathExpr.length < 2)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var hasMath = /[+\-*/^%]/.test(query) && /\d/.test(query);
|
||||||
|
if (!hasMath)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var sanitized = mathExpr.replace(/\^/g, "**");
|
||||||
|
var result = Function('"use strict"; return (' + sanitized + ')')();
|
||||||
|
|
||||||
|
if (typeof result === "number" && isFinite(result)) {
|
||||||
|
var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, "");
|
||||||
|
return {
|
||||||
|
expression: query,
|
||||||
|
result: result,
|
||||||
|
displayResult: displayResult
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortPluginIdsByOrder(pluginIds, order) {
|
||||||
|
if (!order || order.length === 0)
|
||||||
|
return pluginIds;
|
||||||
|
var orderMap = {};
|
||||||
|
for (var i = 0; i < order.length; i++)
|
||||||
|
orderMap[order[i]] = i;
|
||||||
|
return pluginIds.slice().sort(function (a, b) {
|
||||||
|
var aOrder = orderMap[a] !== undefined ? orderMap[a] : 9999;
|
||||||
|
var bOrder = orderMap[b] !== undefined ? orderMap[b] : 9999;
|
||||||
|
return aOrder - bOrder;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortPluginsOrdered(plugins, order) {
|
||||||
|
if (!order || order.length === 0)
|
||||||
|
return plugins;
|
||||||
|
var orderMap = {};
|
||||||
|
for (var i = 0; i < order.length; i++)
|
||||||
|
orderMap[order[i]] = i;
|
||||||
|
return plugins.sort(function (a, b) {
|
||||||
|
var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999;
|
||||||
|
var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999;
|
||||||
|
return aOrder - bOrder;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -186,6 +186,14 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleWithQuery(query) {
|
||||||
|
if (spotlightOpen) {
|
||||||
|
hide();
|
||||||
|
} else {
|
||||||
|
showWithQuery(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: closeCleanupTimer
|
id: closeCleanupTimer
|
||||||
interval: Theme.expressiveDurations.expressiveFastSpatial + 50
|
interval: Theme.expressiveDurations.expressiveFastSpatial + 50
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Rectangle {
|
|||||||
readonly property int computedIconSize: Math.min(48, Math.max(32, width * 0.45))
|
readonly property int computedIconSize: Math.min(48, Math.max(32, width * 0.45))
|
||||||
|
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent"
|
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent"
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|||||||
223
quickshell/Modals/DankLauncherV2/ItemTransformers.js
Normal file
223
quickshell/Modals/DankLauncherV2/ItemTransformers.js
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
.pragma library
|
||||||
|
|
||||||
|
.import "ControllerUtils.js" as Utils
|
||||||
|
|
||||||
|
function transformApp(app, override, defaultActions, primaryActionLabel) {
|
||||||
|
var appId = app.id || app.execString || app.exec || "";
|
||||||
|
|
||||||
|
var actions = [];
|
||||||
|
if (app.actions && app.actions.length > 0) {
|
||||||
|
for (var i = 0; i < app.actions.length; i++) {
|
||||||
|
actions.push({
|
||||||
|
name: app.actions[i].name,
|
||||||
|
icon: "play_arrow",
|
||||||
|
actionData: app.actions[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: appId,
|
||||||
|
type: "app",
|
||||||
|
name: override?.name || app.name || "",
|
||||||
|
subtitle: override?.comment || app.comment || "",
|
||||||
|
icon: override?.icon || app.icon || "application-x-executable",
|
||||||
|
iconType: "image",
|
||||||
|
section: "apps",
|
||||||
|
data: app,
|
||||||
|
keywords: app.keywords || [],
|
||||||
|
actions: actions,
|
||||||
|
primaryAction: {
|
||||||
|
name: primaryActionLabel,
|
||||||
|
icon: "open_in_new",
|
||||||
|
action: "launch"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformCoreApp(app, openLabel) {
|
||||||
|
var iconName = "apps";
|
||||||
|
var iconType = "material";
|
||||||
|
|
||||||
|
if (app.icon) {
|
||||||
|
if (app.icon.startsWith("svg+corner:")) {
|
||||||
|
iconType = "composite";
|
||||||
|
} else if (app.icon.startsWith("material:")) {
|
||||||
|
iconName = app.icon.substring(9);
|
||||||
|
} else {
|
||||||
|
iconName = app.icon;
|
||||||
|
iconType = "image";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: app.builtInPluginId || app.action || "",
|
||||||
|
type: "app",
|
||||||
|
name: app.name || "",
|
||||||
|
subtitle: app.comment || "",
|
||||||
|
icon: iconName,
|
||||||
|
iconType: iconType,
|
||||||
|
iconFull: app.icon,
|
||||||
|
section: "apps",
|
||||||
|
data: app,
|
||||||
|
isCore: true,
|
||||||
|
actions: [],
|
||||||
|
primaryAction: {
|
||||||
|
name: openLabel,
|
||||||
|
icon: "open_in_new",
|
||||||
|
action: "launch"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformBuiltInLauncherItem(item, pluginId, openLabel) {
|
||||||
|
var rawIcon = item.icon || "extension";
|
||||||
|
var icon = Utils.stripIconPrefix(rawIcon);
|
||||||
|
var iconType = item.iconType;
|
||||||
|
if (!iconType) {
|
||||||
|
if (rawIcon.startsWith("material:"))
|
||||||
|
iconType = "material";
|
||||||
|
else if (rawIcon.startsWith("unicode:"))
|
||||||
|
iconType = "unicode";
|
||||||
|
else
|
||||||
|
iconType = "image";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.action || "",
|
||||||
|
type: "plugin",
|
||||||
|
name: item.name || "",
|
||||||
|
subtitle: item.comment || "",
|
||||||
|
icon: icon,
|
||||||
|
iconType: iconType,
|
||||||
|
section: "plugin_" + pluginId,
|
||||||
|
data: item,
|
||||||
|
pluginId: pluginId,
|
||||||
|
isBuiltInLauncher: true,
|
||||||
|
keywords: item.keywords || [],
|
||||||
|
actions: [],
|
||||||
|
primaryAction: {
|
||||||
|
name: openLabel,
|
||||||
|
icon: "open_in_new",
|
||||||
|
action: "execute"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel) {
|
||||||
|
var filename = file.path ? file.path.split("/").pop() : "";
|
||||||
|
var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: file.path || "",
|
||||||
|
type: "file",
|
||||||
|
name: filename,
|
||||||
|
subtitle: dirname,
|
||||||
|
icon: Utils.getFileIcon(filename),
|
||||||
|
iconType: "material",
|
||||||
|
section: "files",
|
||||||
|
data: file,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: openFolderLabel,
|
||||||
|
icon: "folder_open",
|
||||||
|
action: "open_folder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: copyPathLabel,
|
||||||
|
icon: "content_copy",
|
||||||
|
action: "copy_path"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
primaryAction: {
|
||||||
|
name: openLabel,
|
||||||
|
icon: "open_in_new",
|
||||||
|
action: "open"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformPluginItem(item, pluginId, selectLabel) {
|
||||||
|
var rawIcon = item.icon || "extension";
|
||||||
|
var icon = Utils.stripIconPrefix(rawIcon);
|
||||||
|
var iconType = item.iconType;
|
||||||
|
if (!iconType) {
|
||||||
|
if (rawIcon.startsWith("material:"))
|
||||||
|
iconType = "material";
|
||||||
|
else if (rawIcon.startsWith("unicode:"))
|
||||||
|
iconType = "unicode";
|
||||||
|
else
|
||||||
|
iconType = "image";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id || item.name || "",
|
||||||
|
type: "plugin",
|
||||||
|
name: item.name || "",
|
||||||
|
subtitle: item.comment || item.description || "",
|
||||||
|
icon: icon,
|
||||||
|
iconType: iconType,
|
||||||
|
section: "plugin_" + pluginId,
|
||||||
|
data: item,
|
||||||
|
pluginId: pluginId,
|
||||||
|
keywords: item.keywords || [],
|
||||||
|
actions: item.actions || [],
|
||||||
|
primaryAction: item.primaryAction || {
|
||||||
|
name: selectLabel,
|
||||||
|
icon: "check",
|
||||||
|
action: "execute"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCalculatorItem(calc, query, copyLabel) {
|
||||||
|
return {
|
||||||
|
id: "calculator_result",
|
||||||
|
type: "calculator",
|
||||||
|
name: calc.displayResult,
|
||||||
|
subtitle: query + " =",
|
||||||
|
icon: "calculate",
|
||||||
|
iconType: "material",
|
||||||
|
section: "calculator",
|
||||||
|
data: {
|
||||||
|
expression: calc.expression,
|
||||||
|
result: calc.result
|
||||||
|
},
|
||||||
|
actions: [],
|
||||||
|
primaryAction: {
|
||||||
|
name: copyLabel,
|
||||||
|
icon: "content_copy",
|
||||||
|
action: "copy"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPluginBrowseItem(pluginId, plugin, trigger, isBuiltIn, isAllowed, browseLabel, triggerLabel, noTriggerLabel) {
|
||||||
|
var rawIcon = isBuiltIn ? (plugin.cornerIcon || "extension") : (plugin.icon || "extension");
|
||||||
|
return {
|
||||||
|
id: "browse_" + pluginId,
|
||||||
|
type: "plugin_browse",
|
||||||
|
name: plugin.name || pluginId,
|
||||||
|
subtitle: trigger ? triggerLabel.replace("%1", trigger) : noTriggerLabel,
|
||||||
|
icon: isBuiltIn ? rawIcon : Utils.stripIconPrefix(rawIcon),
|
||||||
|
iconType: isBuiltIn ? "material" : Utils.detectIconType(rawIcon),
|
||||||
|
section: "browse_plugins",
|
||||||
|
data: {
|
||||||
|
pluginId: pluginId,
|
||||||
|
plugin: plugin,
|
||||||
|
isBuiltIn: isBuiltIn
|
||||||
|
},
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: "All",
|
||||||
|
icon: isAllowed ? "visibility" : "visibility_off",
|
||||||
|
action: "toggle_all_visibility"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
primaryAction: {
|
||||||
|
name: browseLabel,
|
||||||
|
icon: "arrow_forward",
|
||||||
|
action: "browse_plugin"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -271,21 +271,32 @@ FocusScope {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: !editMode
|
visible: !editMode
|
||||||
|
|
||||||
Rectangle {
|
Item {
|
||||||
id: footerBar
|
id: footerBar
|
||||||
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: root.parentModal?.borderWidth ?? 1
|
||||||
|
anchors.rightMargin: root.parentModal?.borderWidth ?? 1
|
||||||
|
anchors.bottomMargin: root.parentModal?.borderWidth ?? 1
|
||||||
readonly property bool showFooter: SettingsData.dankLauncherV2Size !== "micro" && SettingsData.dankLauncherV2ShowFooter
|
readonly property bool showFooter: SettingsData.dankLauncherV2Size !== "micro" && SettingsData.dankLauncherV2ShowFooter
|
||||||
height: showFooter ? 32 : 0
|
height: showFooter ? 36 : 0
|
||||||
visible: showFooter
|
visible: showFooter
|
||||||
color: Theme.surfaceContainerHigh
|
clip: true
|
||||||
radius: Theme.cornerRadius
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: -Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: modeButtonsRow
|
id: modeButtonsRow
|
||||||
x: I18n.isRtl ? parent.width - width - Theme.spacingS : Theme.spacingS
|
anchors.left: parent.left
|
||||||
y: (parent.height - height) / 2
|
anchors.leftMargin: Theme.spacingXS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight
|
||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
@@ -316,28 +327,25 @@ FocusScope {
|
|||||||
required property var modelData
|
required property var modelData
|
||||||
required property int index
|
required property int index
|
||||||
|
|
||||||
width: modeButtonMetrics.width + 14 + Theme.spacingXS + Theme.spacingM * 2 + Theme.spacingS
|
width: buttonContent.width + Theme.spacingM * 2
|
||||||
height: footerBar.height - 4
|
height: 28
|
||||||
radius: Theme.cornerRadius - 2
|
radius: Theme.cornerRadius
|
||||||
color: controller.searchMode === modelData.id || modeArea.containsMouse ? Theme.primaryContainer : "transparent"
|
color: controller.searchMode === modelData.id || modeArea.containsMouse ? Theme.primaryContainer : "transparent"
|
||||||
|
|
||||||
TextMetrics {
|
|
||||||
id: modeButtonMetrics
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
text: modelData.label
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
id: buttonContent
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
name: modelData.icon
|
name: modelData.icon
|
||||||
size: 14
|
size: 14
|
||||||
color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceVariantText
|
color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceVariantText
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: modelData.label
|
text: modelData.label
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceText
|
color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceText
|
||||||
@@ -357,23 +365,28 @@ FocusScope {
|
|||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: hintsRow
|
id: hintsRow
|
||||||
x: I18n.isRtl ? Theme.spacingS : parent.width - width - Theme.spacingS
|
anchors.right: parent.right
|
||||||
y: (parent.height - height) / 2
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: "↑↓ " + I18n.tr("nav")
|
text: "↑↓ " + I18n.tr("nav")
|
||||||
font.pixelSize: Theme.fontSizeSmall - 1
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: "↵ " + I18n.tr("open")
|
text: "↵ " + I18n.tr("open")
|
||||||
font.pixelSize: Theme.fontSizeSmall - 1
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: "Tab " + I18n.tr("actions")
|
text: "Tab " + I18n.tr("actions")
|
||||||
font.pixelSize: Theme.fontSizeSmall - 1
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
@@ -390,7 +403,6 @@ FocusScope {
|
|||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
anchors.rightMargin: Theme.spacingM
|
anchors.rightMargin: Theme.spacingM
|
||||||
anchors.topMargin: Theme.spacingM
|
anchors.topMargin: Theme.spacingM
|
||||||
anchors.bottomMargin: Theme.spacingXS
|
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
clip: false
|
clip: false
|
||||||
|
|
||||||
@@ -483,9 +495,64 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: categoryRow
|
||||||
|
width: parent.width
|
||||||
|
height: controller.activePluginCategories.length > 0 ? 36 : 0
|
||||||
|
visible: controller.activePluginCategories.length > 0
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankDropdown {
|
||||||
|
id: categoryDropdown
|
||||||
|
width: Math.min(200, parent.width)
|
||||||
|
compactMode: true
|
||||||
|
dropdownWidth: 200
|
||||||
|
popupWidth: 240
|
||||||
|
maxPopupHeight: 300
|
||||||
|
enableFuzzySearch: controller.activePluginCategories.length > 8
|
||||||
|
currentValue: {
|
||||||
|
const cats = controller.activePluginCategories;
|
||||||
|
const current = controller.activePluginCategory;
|
||||||
|
if (!current)
|
||||||
|
return cats.length > 0 ? cats[0].name : "";
|
||||||
|
for (let i = 0; i < cats.length; i++) {
|
||||||
|
if (cats[i].id === current)
|
||||||
|
return cats[i].name;
|
||||||
|
}
|
||||||
|
return cats.length > 0 ? cats[0].name : "";
|
||||||
|
}
|
||||||
|
options: {
|
||||||
|
const cats = controller.activePluginCategories;
|
||||||
|
const names = [];
|
||||||
|
for (let i = 0; i < cats.length; i++)
|
||||||
|
names.push(cats[i].name);
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
onValueChanged: value => {
|
||||||
|
const cats = controller.activePluginCategories;
|
||||||
|
for (let i = 0; i < cats.length; i++) {
|
||||||
|
if (cats[i].name === value) {
|
||||||
|
controller.setActivePluginCategory(cats[i].id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - searchField.height - actionPanel.height - Theme.spacingXS * 2
|
height: parent.height - searchField.height - categoryRow.height - actionPanel.height - Theme.spacingXS * (categoryRow.visible ? 3 : 2)
|
||||||
opacity: root.parentModal?.isClosing ? 0 : 1
|
opacity: root.parentModal?.isClosing ? 0 : 1
|
||||||
|
|
||||||
ResultsList {
|
ResultsList {
|
||||||
|
|||||||
@@ -64,11 +64,18 @@ Popup {
|
|||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function executePluginAction(actionFunc) {
|
function executePluginAction(actionOrObj) {
|
||||||
if (typeof actionFunc === "function") {
|
var actionFunc = typeof actionOrObj === "function" ? actionOrObj : actionOrObj?.action;
|
||||||
|
var closeLauncher = typeof actionOrObj === "object" && actionOrObj?.closeLauncher;
|
||||||
|
|
||||||
|
if (typeof actionFunc === "function")
|
||||||
actionFunc();
|
actionFunc();
|
||||||
|
|
||||||
|
if (closeLauncher) {
|
||||||
|
controller?.itemExecuted();
|
||||||
|
} else {
|
||||||
|
controller?.performSearch();
|
||||||
}
|
}
|
||||||
controller?.performSearch();
|
|
||||||
hide();
|
hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +90,7 @@ Popup {
|
|||||||
type: "item",
|
type: "item",
|
||||||
icon: act.icon || "play_arrow",
|
icon: act.icon || "play_arrow",
|
||||||
text: act.text || act.name || "",
|
text: act.text || act.name || "",
|
||||||
pluginAction: act.action
|
pluginAction: act
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
@@ -131,6 +138,16 @@ Popup {
|
|||||||
items.push({
|
items.push({
|
||||||
type: "separator"
|
type: "separator"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isRegularApp && SessionService.nvidiaCommand) {
|
||||||
|
items.push({
|
||||||
|
type: "item",
|
||||||
|
icon: "memory",
|
||||||
|
text: I18n.tr("Launch on dGPU"),
|
||||||
|
action: launchWithNvidia
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
type: "item",
|
type: "item",
|
||||||
icon: "launch",
|
icon: "launch",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user