1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-14 16:22:46 -04:00

Compare commits

..

2 Commits

Author SHA1 Message Date
purian23 62bf9c6efe Add Directional Motion options 2026-03-04 10:14:00 -05:00
purian23 61a77bd186 Initial staging for Animation & Motion effects 2026-03-03 20:02:32 -05:00
189 changed files with 5541 additions and 36174 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ steps.app_token.outputs.token }} token: ${{ steps.app_token.outputs.token }}
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Install flatpak - name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak run: sudo apt update && sudo apt install -y flatpak
@@ -38,7 +38,7 @@ jobs:
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 - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
+2 -2
View File
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Install flatpak - name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak run: sudo apt update && sudo apt install -y flatpak
@@ -21,7 +21,7 @@ jobs:
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 - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version-file: core/go.mod go-version-file: core/go.mod
+8 -8
View File
@@ -32,13 +32,13 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
ref: ${{ inputs.tag }} ref: ${{ inputs.tag }}
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
@@ -106,7 +106,7 @@ jobs:
- name: Upload artifacts (${{ matrix.arch }}) - name: Upload artifacts (${{ matrix.arch }})
if: matrix.arch == 'arm64' if: matrix.arch == 'arm64'
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
with: with:
name: core-assets-${{ matrix.arch }} name: core-assets-${{ matrix.arch }}
path: | path: |
@@ -120,7 +120,7 @@ jobs:
- name: Upload artifacts with completions - name: Upload artifacts with completions
if: matrix.arch == 'amd64' if: matrix.arch == 'amd64'
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
with: with:
name: core-assets-${{ matrix.arch }} name: core-assets-${{ matrix.arch }}
path: | path: |
@@ -147,7 +147,7 @@ jobs:
# private-key: ${{ secrets.APP_PRIVATE_KEY }} # private-key: ${{ secrets.APP_PRIVATE_KEY }}
# - name: Checkout # - name: Checkout
# uses: actions/checkout@v6 # uses: actions/checkout@v4
# with: # with:
# token: ${{ steps.app_token.outputs.token }} # token: ${{ steps.app_token.outputs.token }}
# fetch-depth: 0 # fetch-depth: 0
@@ -181,7 +181,7 @@ jobs:
TAG: ${{ inputs.tag }} TAG: ${{ inputs.tag }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
ref: ${{ inputs.tag }} ref: ${{ inputs.tag }}
fetch-depth: 0 fetch-depth: 0
@@ -192,12 +192,12 @@ jobs:
git checkout ${TAG} git checkout ${TAG}
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
- name: Download core artifacts - name: Download core artifacts
uses: actions/download-artifact@v5 uses: actions/download-artifact@v4
with: with:
pattern: core-assets-* pattern: core-assets-*
merge-multiple: true merge-multiple: true
+2 -2
View File
@@ -46,7 +46,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Determine version - name: Determine version
id: version id: version
@@ -134,7 +134,7 @@ jobs:
rpm -qpi "$SRPM" rpm -qpi "$SRPM"
- name: Upload SRPM artifact - name: Upload SRPM artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
with: with:
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }} name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
path: ${{ steps.build.outputs.srpm_path }} path: ${{ steps.build.outputs.srpm_path }}
+3 -6
View File
@@ -32,7 +32,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -195,13 +195,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Wait before OBS upload
run: sleep 3
- name: Determine packages to update - name: Determine packages to update
id: packages id: packages
run: | run: |
@@ -347,7 +344,7 @@ jobs:
done done
- name: Install Go - name: Install Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
+3 -3
View File
@@ -31,7 +31,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -157,12 +157,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
cache: false cache: false
+4 -4
View File
@@ -24,7 +24,7 @@ jobs:
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ steps.app_token.outputs.token }} token: ${{ steps.app_token.outputs.token }}
@@ -40,7 +40,7 @@ jobs:
echo "Build succeeded, no hash update needed" echo "Build succeeded, no hash update needed"
exit 0 exit 0
fi fi
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1 || true) new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; } [ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix) current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; } [ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
@@ -59,8 +59,8 @@ jobs:
git config user.email "dms-ci[bot]@users.noreply.github.com" git config user.email "dms-ci[bot]@users.noreply.github.com"
git add flake.nix git add flake.nix
git commit -m "nix: update vendorHash for go.mod changes" || exit 0 git commit -m "nix: update vendorHash for go.mod changes" || exit 0
git pull --rebase origin ${{ github.ref_name }} git pull --rebase origin master
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }} git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
else else
echo "No changes to flake.nix" echo "No changes to flake.nix"
fi fi
+1 -3
View File
@@ -86,9 +86,7 @@ touch .qmlls.ini
4. Restart dms to generate the `.qmlls.ini` file 4. Restart dms to generate the `.qmlls.ini` file
5. Run `make lint-qml` from the repo root to lint QML entrypoints (requires the `.qmlls.ini` generated above). The script needs the **Qt 6** `qmllint`; it checks `qmllint6`, Fedora's `qmllint-qt6`, `/usr/lib/qt6/bin/qmllint`, then `qmllint` in `PATH`. If your Qt 6 binary lives elsewhere, set `QMLLINT=/path/to/qmllint`. 5. Make your changes, test, and open a pull request.
6. Make your changes, test, and open a pull request.
### I18n/Localization ### I18n/Localization
+2 -6
View File
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
ASSETS_DIR=assets ASSETS_DIR=assets
APPLICATIONS_DIR=$(DATA_DIR)/applications APPLICATIONS_DIR=$(DATA_DIR)/applications
.PHONY: all build clean lint-qml install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help .PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
all: build all: build
@@ -32,9 +32,6 @@ clean:
@$(MAKE) -C $(CORE_DIR) clean @$(MAKE) -C $(CORE_DIR) clean
@echo "Clean complete" @echo "Clean complete"
lint-qml:
@./quickshell/scripts/qmllint-entrypoints.sh
# Installation targets # Installation targets
install-bin: install-bin:
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@@ -79,7 +76,7 @@ install-desktop:
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true @update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entry installed" @echo "Desktop entry installed"
install: install-bin install-shell install-completions install-systemd install-icon install-desktop install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
@echo "" @echo ""
@echo "Installation complete!" @echo "Installation complete!"
@echo "" @echo ""
@@ -133,7 +130,6 @@ help:
@echo " all (default) - Build the DMS binary" @echo " all (default) - Build the DMS binary"
@echo " build - Same as 'all'" @echo " build - Same as 'all'"
@echo " clean - Clean build artifacts" @echo " clean - Clean build artifacts"
@echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS"
@echo "" @echo ""
@echo "Install:" @echo "Install:"
@echo " install - Build and install everything (requires sudo)" @echo " install - Build and install everything (requires sudo)"
+4 -17
View File
@@ -1,26 +1,13 @@
repos: repos:
- repo: local - repo: https://github.com/golangci/golangci-lint
rev: v2.10.1
hooks: hooks:
- id: golangci-lint-fmt - id: golangci-lint-fmt
name: golangci-lint-fmt
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 fmt
language: system
require_serial: true require_serial: true
types: [go]
pass_filenames: false
- id: golangci-lint-full - id: golangci-lint-full
name: golangci-lint-full
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 run --fix
language: system
require_serial: true
types: [go]
pass_filenames: false
- id: golangci-lint-config-verify - id: golangci-lint-config-verify
name: golangci-lint-config-verify - repo: local
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 config verify hooks:
language: system
files: \.golangci\.(?:yml|yaml|toml|json)
pass_filenames: false
- id: go-test - id: go-test
name: go test name: go test
entry: go test ./... entry: go test ./...
+3 -3
View File
@@ -63,19 +63,19 @@ endif
build-all: build dankinstall build-all: build dankinstall
install: install: build
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME) @install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Installation complete" @echo "Installation complete"
install-all: install-all: build-all
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME) @install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL) @install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
@echo "Installation complete" @echo "Installation complete"
install-dankinstall: install-dankinstall: dankinstall
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL) @install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
@echo "Installation complete" @echo "Installation complete"
@@ -1,10 +0,0 @@
{
"policy_version": 1,
"blocked_commands": [
"greeter install",
"greeter enable",
"greeter uninstall",
"setup"
],
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
}
+1 -11
View File
@@ -222,19 +222,16 @@ func init() {
func runClipCopy(cmd *cobra.Command, args []string) { func runClipCopy(cmd *cobra.Command, args []string) {
var data []byte var data []byte
copyFromStdin := false
switch { switch {
case len(args) > 0: case len(args) > 0:
data = []byte(args[0]) data = []byte(args[0])
case clipCopyDownload || clipCopyType == "__multi__": 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 {
log.Fatalf("read stdin: %v", err) log.Fatalf("read stdin: %v", err)
} }
default:
copyFromStdin = true
} }
if clipCopyDownload { if clipCopyDownload {
@@ -260,13 +257,6 @@ func runClipCopy(cmd *cobra.Command, args []string) {
return return
} }
if copyFromStdin {
if err := clipboard.CopyReader(os.Stdin, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
log.Fatalf("copy: %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)
} }
+5 -5
View File
@@ -1079,14 +1079,14 @@ func formatResultsPlain(results []checkResult) string {
if currentCategory != -1 { if currentCategory != -1 {
sb.WriteString("\n") sb.WriteString("\n")
} }
fmt.Fprintf(&sb, "**%s**\n", r.category.String()) sb.WriteString(fmt.Sprintf("**%s**\n", r.category.String()))
currentCategory = r.category currentCategory = r.category
} }
fmt.Fprintf(&sb, "- [%s] %s: %s\n", r.status, r.name, r.message) sb.WriteString(fmt.Sprintf("- [%s] %s: %s\n", r.status, r.name, r.message))
if doctorVerbose && r.details != "" { if doctorVerbose && r.details != "" {
fmt.Fprintf(&sb, " - %s\n", r.details) sb.WriteString(fmt.Sprintf(" - %s\n", r.details))
} }
} }
@@ -1096,8 +1096,8 @@ func formatResultsPlain(results []checkResult) string {
} }
sb.WriteString("\n---\n") sb.WriteString("\n---\n")
fmt.Fprintf(&sb, "**Summary:** %d error(s), %d warning(s), %d ok\n", sb.WriteString(fmt.Sprintf("**Summary:** %d error(s), %d warning(s), %d ok\n",
ds.ErrorCount(), ds.WarningCount(), ds.OKCount()) ds.ErrorCount(), ds.WarningCount(), ds.OKCount()))
return sb.String() return sb.String()
} }
File diff suppressed because it is too large Load Diff
+1 -5
View File
@@ -57,11 +57,10 @@ func init() {
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal") cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant") cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip") cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
cmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
} }
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion") matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting") matugenQueueCmd.Flags().Duration("timeout", 30*time.Second, "Timeout for waiting")
} }
func buildMatugenOptions(cmd *cobra.Command) matugen.Options { func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
@@ -78,7 +77,6 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal") syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark") terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
skipTemplates, _ := cmd.Flags().GetString("skip-templates") skipTemplates, _ := cmd.Flags().GetString("skip-templates")
contrast, _ := cmd.Flags().GetFloat64("contrast")
return matugen.Options{ return matugen.Options{
StateDir: stateDir, StateDir: stateDir,
@@ -89,7 +87,6 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
Mode: matugen.ColorMode(mode), Mode: matugen.ColorMode(mode),
IconTheme: iconTheme, IconTheme: iconTheme,
MatugenType: matugenType, MatugenType: matugenType,
Contrast: contrast,
RunUserTemplates: runUserTemplates, RunUserTemplates: runUserTemplates,
StockColors: stockColors, StockColors: stockColors,
SyncModeWithPortal: syncModeWithPortal, SyncModeWithPortal: syncModeWithPortal,
@@ -131,7 +128,6 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
"syncModeWithPortal": opts.SyncModeWithPortal, "syncModeWithPortal": opts.SyncModeWithPortal,
"terminalsAlwaysDark": opts.TerminalsAlwaysDark, "terminalsAlwaysDark": opts.TerminalsAlwaysDark,
"skipTemplates": opts.SkipTemplates, "skipTemplates": opts.SkipTemplates,
"contrast": opts.Contrast,
"wait": wait, "wait": wait,
}, },
} }
+3 -4
View File
@@ -16,10 +16,9 @@ import (
) )
var setupCmd = &cobra.Command{ var setupCmd = &cobra.Command{
Use: "setup", Use: "setup",
Short: "Deploy DMS configurations", Short: "Deploy DMS configurations",
Long: "Deploy compositor and terminal configurations with interactive prompts", Long: "Deploy compositor and terminal configurations with interactive prompts",
PersistentPreRunE: requireMutableSystemCommand,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if err := runSetup(); err != nil { if err := runSetup(); err != nil {
log.Fatalf("Error during setup: %v", err) log.Fatalf("Error during setup: %v", err)
-271
View File
@@ -1,271 +0,0 @@
package main
import (
"bufio"
_ "embed"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"github.com/spf13/cobra"
)
const (
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
)
var (
immutablePolicyOnce sync.Once
immutablePolicy immutableCommandPolicy
immutablePolicyErr error
)
//go:embed assets/cli-policy.default.json
var defaultCLIPolicyJSON []byte
type immutableCommandPolicy struct {
ImmutableSystem bool
ImmutableReason string
BlockedCommands []string
Message string
}
type cliPolicyFile struct {
PolicyVersion int `json:"policy_version"`
ImmutableSystem *bool `json:"immutable_system"`
BlockedCommands *[]string `json:"blocked_commands"`
Message *string `json:"message"`
}
func normalizeCommandSpec(raw string) string {
normalized := strings.ToLower(strings.TrimSpace(raw))
normalized = strings.TrimPrefix(normalized, "dms ")
return strings.Join(strings.Fields(normalized), " ")
}
func normalizeBlockedCommands(raw []string) []string {
normalized := make([]string, 0, len(raw))
seen := make(map[string]bool)
for _, cmd := range raw {
spec := normalizeCommandSpec(cmd)
if spec == "" || seen[spec] {
continue
}
seen[spec] = true
normalized = append(normalized, spec)
}
return normalized
}
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
normalizedPath := normalizeCommandSpec(commandPath)
if normalizedPath == "" {
return false
}
for _, entry := range blocked {
spec := normalizeCommandSpec(entry)
if spec == "" {
continue
}
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
return true
}
}
return false
}
func loadPolicyFile(path string) (*cliPolicyFile, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("failed to read %s: %w", path, err)
}
var policy cliPolicyFile
if err := json.Unmarshal(data, &policy); err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
}
return &policy, nil
}
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
policyFile, err := loadPolicyFile(path)
if err != nil {
return err
}
if policyFile == nil {
return nil
}
if policyFile.ImmutableSystem != nil {
base.ImmutableSystem = *policyFile.ImmutableSystem
}
if policyFile.BlockedCommands != nil {
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
}
if policyFile.Message != nil {
msg := strings.TrimSpace(*policyFile.Message)
if msg != "" {
base.Message = msg
}
}
return nil
}
func readOSReleaseMap(path string) map[string]string {
values := make(map[string]string)
file, err := os.Open(path)
if err != nil {
return values
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.ToUpper(strings.TrimSpace(parts[0]))
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
values[key] = strings.ToLower(value)
}
return values
}
func hasAnyToken(text string, tokens ...string) bool {
if text == "" {
return false
}
for _, token := range tokens {
if strings.Contains(text, token) {
return true
}
}
return false
}
func detectImmutableSystem() (bool, string) {
if _, err := os.Stat("/run/ostree-booted"); err == nil {
return true, "/run/ostree-booted is present"
}
osRelease := readOSReleaseMap("/etc/os-release")
if len(osRelease) == 0 {
return false, ""
}
id := osRelease["ID"]
idLike := osRelease["ID_LIKE"]
variantID := osRelease["VARIANT_ID"]
name := osRelease["NAME"]
prettyName := osRelease["PRETTY_NAME"]
immutableIDs := map[string]bool{
"bluefin": true,
"bazzite": true,
"silverblue": true,
"kinoite": true,
"sericea": true,
"onyx": true,
"aurora": true,
"fedora-iot": true,
"fedora-coreos": true,
}
if immutableIDs[id] {
return true, "os-release ID=" + id
}
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
if hasAnyToken(variantID, markers...) {
return true, "os-release VARIANT_ID=" + variantID
}
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
return true, "os-release ID_LIKE=" + idLike
}
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
return true, "os-release identifies an atomic/ostree variant"
}
return false, ""
}
func getImmutablePolicy() (*immutableCommandPolicy, error) {
immutablePolicyOnce.Do(func() {
detectedImmutable, reason := detectImmutableSystem()
immutablePolicy = immutableCommandPolicy{
ImmutableSystem: detectedImmutable,
ImmutableReason: reason,
BlockedCommands: []string{"greeter install", "greeter enable", "setup"},
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
}
var defaultPolicy cliPolicyFile
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
return
}
if defaultPolicy.BlockedCommands != nil {
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
}
if defaultPolicy.Message != nil {
msg := strings.TrimSpace(*defaultPolicy.Message)
if msg != "" {
immutablePolicy.Message = msg
}
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
immutablePolicyErr = err
return
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
immutablePolicyErr = err
return
}
})
if immutablePolicyErr != nil {
return nil, immutablePolicyErr
}
return &immutablePolicy, nil
}
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
policy, err := getImmutablePolicy()
if err != nil {
return err
}
if !policy.ImmutableSystem {
return nil
}
commandPath := normalizeCommandSpec(cmd.CommandPath())
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
return nil
}
reason := ""
if policy.ImmutableReason != "" {
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
}
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
}
+10 -1
View File
@@ -16,10 +16,19 @@ func init() {
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) // Add subcommands to greeter
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to update
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(updateCmd) rootCmd.AddCommand(updateCmd)
+10 -1
View File
@@ -11,20 +11,29 @@ import (
var Version = "dev" var Version = "dev"
func init() { func init() {
// Add flags
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode") runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process") runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) // Add subcommands to greeter
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.SetHelpTemplate(getHelpTemplate()) rootCmd.SetHelpTemplate(getHelpTemplate())
} }
func main() { func main() {
// Block root
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
log.Fatal("This program should not be run as root. Exiting.") log.Fatal("This program should not be run as root. Exiting.")
} }
+8
View File
@@ -7,6 +7,14 @@ import (
"strings" "strings"
) )
func findCommandPath(cmd string) (string, error) {
path, err := exec.LookPath(cmd)
if err != nil {
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
}
return path, nil
}
func isArchPackageInstalled(packageName string) bool { func isArchPackageInstalled(packageName string) bool {
cmd := exec.Command("pacman", "-Q", packageName) cmd := exec.Command("pacman", "-Q", packageName)
err := cmd.Run() err := cmd.Run()
+1 -3
View File
@@ -1,8 +1,6 @@
module github.com/AvengeMedia/DankMaterialShell/core module github.com/AvengeMedia/DankMaterialShell/core
go 1.26.0 go 1.25.0
toolchain go1.26.1
require ( require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0 github.com/Wifx/gonetworkmanager/v2 v2.2.0
+8 -92
View File
@@ -1,12 +1,10 @@
package clipboard package clipboard
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"syscall" "syscall"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
@@ -14,37 +12,17 @@ import (
) )
func Copy(data []byte, mimeType string) error { func Copy(data []byte, mimeType string) error {
return CopyReader(bytes.NewReader(data), mimeType, false, false) return CopyOpts(data, mimeType, false, false)
} }
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error { func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
if foreground {
return copyServeWithWriter(func(writer io.Writer) error {
total := 0
for total < len(data) {
n, err := writer.Write(data[total:])
total += n
if err != nil {
return err
}
}
if total != len(data) {
return io.ErrShortWrite
}
return nil
}, mimeType, pasteOnce)
}
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
}
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
if !foreground { if !foreground {
return copyFork(data, mimeType, pasteOnce) return copyFork(data, mimeType, pasteOnce)
} }
return copyServeReader(data, mimeType, pasteOnce) return copyServe(data, mimeType, pasteOnce)
} }
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error { func copyFork(data []byte, mimeType string, pasteOnce bool) error {
args := []string{os.Args[0], "cl", "copy", "--foreground"} args := []string{os.Args[0], "cl", "copy", "--foreground"}
if pasteOnce { if pasteOnce {
args = append(args, "--paste-once") args = append(args, "--paste-once")
@@ -52,15 +30,11 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
args = append(args, "--type", mimeType) args = append(args, "--type", mimeType)
cmd := exec.Command(args[0], args[1:]...) cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = nil
cmd.Stdout = nil cmd.Stdout = nil
cmd.Stderr = nil cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
if stdinSource, ok := data.(*os.File); ok {
cmd.Stdin = stdinSource
return cmd.Start()
}
stdin, err := cmd.StdinPipe() stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {
return fmt.Errorf("stdin pipe: %w", err) return fmt.Errorf("stdin pipe: %w", err)
@@ -70,66 +44,16 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
return fmt.Errorf("start: %w", err) return fmt.Errorf("start: %w", err)
} }
if _, err := io.Copy(stdin, data); err != nil { if _, err := stdin.Write(data); err != nil {
stdin.Close() stdin.Close()
return fmt.Errorf("write stdin: %w", err) return fmt.Errorf("write stdin: %w", err)
} }
if err := stdin.Close(); err != nil { stdin.Close()
return fmt.Errorf("close stdin: %w", err)
}
return nil return nil
} }
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error { func copyServe(data []byte, mimeType string, pasteOnce bool) error {
cachedData, err := createClipboardCacheFile()
if err != nil {
return fmt.Errorf("create clipboard cache file: %w", err)
}
defer os.Remove(cachedData.Name())
if _, err := io.Copy(cachedData, data); err != nil {
return fmt.Errorf("cache clipboard data: %w", err)
}
if err := cachedData.Close(); err != nil {
return fmt.Errorf("close temp cache file: %w", err)
}
return copyServeWithWriter(func(writer io.Writer) error {
cachedFile, err := os.Open(cachedData.Name())
if err != nil {
return fmt.Errorf("open temp cache file: %w", err)
}
defer cachedFile.Close()
if _, err := io.Copy(writer, cachedFile); err != nil {
return fmt.Errorf("write clipboard data: %w", err)
}
return nil
}, mimeType, pasteOnce)
}
func createClipboardCacheFile() (*os.File, error) {
preferredDirs := []string{}
if cacheDir, err := os.UserCacheDir(); err == nil {
preferredDirs = append(preferredDirs, filepath.Join(cacheDir, "dms", "clipboard"))
}
preferredDirs = append(preferredDirs, "/var/tmp/dms/clipboard")
for _, dir := range preferredDirs {
if err := os.MkdirAll(dir, 0o700); err != nil {
continue
}
cachedData, err := os.CreateTemp(dir, "dms-clipboard-*")
if err == nil {
return cachedData, nil
}
}
return os.CreateTemp("", "dms-clipboard-*")
}
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
display, err := wlclient.Connect("") display, err := wlclient.Connect("")
if err != nil { if err != nil {
return fmt.Errorf("wayland connect: %w", err) return fmt.Errorf("wayland connect: %w", err)
@@ -215,18 +139,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
cancelled := make(chan struct{}) cancelled := make(chan struct{})
pasted := make(chan struct{}, 1) pasted := make(chan struct{}, 1)
sendErr := make(chan error, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) { source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
defer syscall.Close(e.Fd) defer syscall.Close(e.Fd)
file := os.NewFile(uintptr(e.Fd), "pipe") file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close() defer file.Close()
if err := writeTo(file); err != nil { file.Write(data)
select {
case sendErr <- err:
default:
}
}
select { select {
case pasted <- struct{}{}: case pasted <- struct{}{}:
default: default:
@@ -247,8 +165,6 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
select { select {
case <-cancelled: case <-cancelled:
return nil return nil
case err := <-sendErr:
return err
case <-pasted: case <-pasted:
if pasteOnce { if pasteOnce {
return nil return nil
+39 -18
View File
@@ -440,10 +440,29 @@ func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []st
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", "))) a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
hasNiri := false hasNiri := false
hasQuickshell := false
for _, pkg := range packages { for _, pkg := range packages {
if pkg == "niri-git" { if pkg == "niri-git" {
hasNiri = true hasNiri = true
} }
if pkg == "quickshell" || pkg == "quickshell-git" {
hasQuickshell = true
}
}
// If quickshell is in the list, always reinstall google-breakpad first
if hasQuickshell {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.63,
Step: "Reinstalling google-breakpad for quickshell...",
IsComplete: false,
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
}
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
}
} }
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed // If niri is in the list, install makepkg-git-lfs-proto first if not already installed
@@ -597,16 +616,10 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err) return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
} }
srcinfoPath = filepath.Join(packageDir, ".SRCINFO") // Skip dependency installation for dms-shell-git and dms-shell-bin
if pkg == "dms-shell-bin" { // since we manually manage those dependencies
progressChan <- InstallProgressMsg{ if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
Phase: PhaseAURPackages, // Pre-install dependencies from .SRCINFO
Progress: startProgress + 0.35*(endProgress-startProgress),
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
IsComplete: false,
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
}
} else {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages, Phase: PhaseAURPackages,
Progress: startProgress + 0.3*(endProgress-startProgress), Progress: startProgress + 0.3*(endProgress-startProgress),
@@ -615,19 +628,19 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
CommandInfo: "Installing package dependencies and makedepends", CommandInfo: "Installing package dependencies and makedepends",
} }
// Install dependencies from .SRCINFO // Install dependencies and makedepends explicitly
depFilter := "" srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
if pkg == "dms-shell-git" {
depFilter = ` | sed -E 's/[[:space:]]*(quickshell|dgop)[[:space:]]*/ /g' | tr -s ' '`
}
depsCmd := exec.CommandContext(ctx, "bash", "-c", depsCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(` fmt.Sprintf(`
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' %s | sed 's/[[:space:]]*$//') deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
if [[ "%s" == *"quickshell"* ]]; then
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
fi
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
fi fi
`, srcinfoPath, depFilter, sudoPassword)) `, srcinfoPath, pkg, sudoPassword))
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil { if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err) return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
@@ -644,6 +657,14 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil { if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err) return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
} }
} else {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress),
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
IsComplete: false,
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
}
} }
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
@@ -656,7 +677,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm") buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
buildCmd.Dir = packageDir buildCmd.Dir = packageDir
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil { if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
return fmt.Errorf("failed to build %s: %w", pkg, err) return fmt.Errorf("failed to build %s: %w", pkg, err)
+18 -69
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"os/exec" "os/exec"
"runtime"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -91,27 +92,9 @@ func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
} }
func (d *DebianDistribution) packageInstalled(pkg string) bool { func (d *DebianDistribution) packageInstalled(pkg string) bool {
return debianPackageInstalledPrecisely(pkg) cmd := exec.Command("dpkg", "-l", pkg)
} err := cmd.Run()
return err == nil
func debianPackageInstalledPrecisely(pkg string) bool {
cmd := exec.Command("dpkg-query", "-W", "-f=${db:Status-Status}", pkg)
output, err := cmd.Output()
if err != nil {
return false
}
return strings.TrimSpace(string(output)) == "installed"
}
func debianRepoArchitecture(arch string) string {
switch arch {
case "amd64", "x86_64":
return "amd64"
case "arm64", "aarch64":
return "arm64"
default:
return arch
}
} }
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
@@ -211,12 +194,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
Step: "Installing development dependencies...", Step: "Installing development dependencies...",
IsComplete: false, IsComplete: false,
NeedsSudo: true, NeedsSudo: true,
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev", CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
LogOutput: "Installing additional development tools", LogOutput: "Installing additional development tools",
} }
devToolsCmd := ExecSudoCommand(ctx, sudoPassword, devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev") "DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil { if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err) return fmt.Errorf("failed to install development tools: %w", err)
} }
@@ -396,14 +379,6 @@ func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []st
return names return names
} }
func (d *DebianDistribution) aptInstallArgs(packages []string, minimal bool) []string {
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
if minimal {
args = append(args, "--no-install-recommends")
}
return append(args, packages...)
}
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool) enabledRepos := make(map[string]bool)
@@ -461,7 +436,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
} }
// Add repository // Add repository
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, debianRepoArchitecture(osInfo.Architecture), baseURL) repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
@@ -507,46 +482,20 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", "))) d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
groups := orderedMinimalInstallGroups(packages) args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
totalGroups := len(groups) args = append(args, packages...)
groupIndex := 0 progressChan <- InstallProgressMsg{
installGroup := func(groupPackages []string, minimal bool) error { Phase: PhaseSystemPackages,
if len(groupPackages) == 0 { Progress: 0.40,
return nil Step: "Installing system packages...",
} IsComplete: false,
NeedsSudo: true,
groupIndex++ CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
startProgress := 0.40
endProgress := 0.60
if totalGroups > 1 {
if groupIndex == 1 {
endProgress = 0.50
} else {
startProgress = 0.50
}
}
args := d.aptInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: startProgress,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
} }
for _, group := range groups { cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
if err := installGroup(group.packages, group.minimal); err != nil { return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
return err
}
}
return nil
} }
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
+42 -52
View File
@@ -484,7 +484,28 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", "))) f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60) args := []string{"dnf", "install", "-y"}
for _, pkg := range packages {
if pkg == "niri" || pkg == "niri-git" {
args = append(args, "--setopt=install_weak_deps=False")
break
}
}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
} }
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -494,57 +515,26 @@ func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages [
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", "))) f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing COPR packages...", 0.70, 0.85)
}
func (f *FedoraDistribution) dnfInstallArgs(packages []string, minimal bool) []string {
args := []string{"dnf", "install", "-y"} args := []string{"dnf", "install", "-y"}
if minimal {
args = append(args, "--setopt=install_weak_deps=False") for _, pkg := range packages {
if pkg == "niri" || pkg == "niri-git" {
args = append(args, "--setopt=install_weak_deps=False")
break
}
} }
return append(args, packages...)
} args = append(args, packages...)
func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error { progressChan <- InstallProgressMsg{
groups := orderedMinimalInstallGroups(packages) Phase: PhaseAURPackages,
totalGroups := len(groups) Progress: 0.70,
Step: "Installing COPR packages...",
groupIndex := 0 IsComplete: false,
installGroup := func(groupPackages []string, minimal bool) error { NeedsSudo: true,
if len(groupPackages) == 0 { CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
return nil }
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
groupIndex++ return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
groupStart := startProgress
groupEnd := endProgress
if totalGroups > 1 {
midpoint := startProgress + ((endProgress - startProgress) / 2)
if groupIndex == 1 {
groupEnd = midpoint
} else {
groupStart = midpoint
}
}
args := f.dnfInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: groupStart,
Step: step,
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
}
for _, group := range groups {
if err := installGroup(group.packages, group.minimal); err != nil {
return err
}
}
return nil
} }
-44
View File
@@ -1,44 +0,0 @@
package distros
type minimalInstallGroup struct {
packages []string
minimal bool
}
func shouldPreferMinimalInstall(pkg string) bool {
switch pkg {
case "niri", "niri-git":
return true
default:
return false
}
}
func splitMinimalInstallPackages(packages []string) (normal []string, minimal []string) {
for _, pkg := range packages {
if shouldPreferMinimalInstall(pkg) {
minimal = append(minimal, pkg)
continue
}
normal = append(normal, pkg)
}
return normal, minimal
}
func orderedMinimalInstallGroups(packages []string) []minimalInstallGroup {
normal, minimal := splitMinimalInstallPackages(packages)
groups := make([]minimalInstallGroup, 0, 2)
if len(minimal) > 0 {
groups = append(groups, minimalInstallGroup{
packages: minimal,
minimal: true,
})
}
if len(normal) > 0 {
groups = append(groups, minimalInstallGroup{
packages: normal,
minimal: false,
})
}
return groups
}
+43 -164
View File
@@ -6,7 +6,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -30,8 +29,6 @@ type OpenSUSEDistribution struct {
config DistroConfig config DistroConfig
} }
const openSUSENiriWaylandServerPackage = "libwayland-server0"
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution { func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
base := NewBaseDistribution(logChan) base := NewBaseDistribution(logChan)
return &OpenSUSEDistribution{ return &OpenSUSEDistribution{
@@ -202,7 +199,35 @@ func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
} }
func (o *OpenSUSEDistribution) getPrerequisites() []string { func (o *OpenSUSEDistribution) getPrerequisites() []string {
return []string{} return []string{
"make",
"unzip",
"gcc",
"gcc-c++",
"cmake",
"ninja",
"pkgconf-pkg-config",
"git",
"qt6-base-devel",
"qt6-declarative-devel",
"qt6-declarative-private-devel",
"qt6-shadertools",
"qt6-shadertools-devel",
"qt6-wayland-devel",
"qt6-waylandclient-private-devel",
"spirv-tools-devel",
"cli11-devel",
"wayland-protocols-devel",
"libgbm-devel",
"libdrm-devel",
"pipewire-devel",
"jemalloc-devel",
"wayland-utils",
"Mesa-libGLESv3-devel",
"pam-devel",
"glib2-devel",
"polkit-devel",
}
} }
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -272,10 +297,6 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
LogOutput: "Starting prerequisite check...", LogOutput: "Starting prerequisite check...",
} }
if err := o.disableInstallMediaRepos(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to disable install media repositories: %w", err)
}
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil { if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err) return fmt.Errorf("failed to install prerequisites: %w", err)
} }
@@ -306,7 +327,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
NeedsSudo: true, NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")), LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
} }
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60); err != nil { if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install zypper packages: %w", err) return fmt.Errorf("failed to install zypper packages: %w", err)
} }
} }
@@ -321,7 +342,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
IsComplete: false, IsComplete: false,
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")), LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
} }
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan, PhaseAURPackages, "Installing OBS packages...", 0.70, 0.85); err != nil { if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install OBS packages: %w", err) return fmt.Errorf("failed to install OBS packages: %w", err)
} }
} }
@@ -411,32 +432,9 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
} }
} }
systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags))
return systemPkgs, obsPkgs, manualPkgs, variantMap return systemPkgs, obsPkgs, manualPkgs, variantMap
} }
func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string]bool) []string {
if wm != deps.WindowManagerNiri || disabledFlags["niri"] {
return nil
}
return []string{openSUSENiriWaylandServerPackage}
}
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
for _, pkg := range extraPkgs {
if slices.Contains(systemPkgs, pkg) || o.packageInstalled(pkg) {
continue
}
o.log(fmt.Sprintf("Adding openSUSE runtime package: %s", pkg))
systemPkgs = append(systemPkgs, pkg)
}
return systemPkgs
}
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string { func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages)) names := make([]string, len(packages))
for i, pkg := range packages { for i, pkg := range packages {
@@ -516,146 +514,27 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
return nil return nil
} }
func isOpenSUSEInstallMediaURI(uri string) bool { func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
normalizedURI := strings.ToLower(strings.TrimSpace(uri))
return strings.HasPrefix(normalizedURI, "cd:/") ||
strings.HasPrefix(normalizedURI, "dvd:/") ||
strings.HasPrefix(normalizedURI, "hd:/") ||
strings.HasPrefix(normalizedURI, "iso:/")
}
func parseZypperInstallMediaAliases(output string) []string {
var aliases []string
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line == "" || !strings.Contains(line, "|") {
continue
}
parts := strings.Split(line, "|")
if len(parts) < 7 {
continue
}
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
alias := parts[1]
enabled := strings.ToLower(parts[3])
uri := parts[len(parts)-1]
if alias == "" || strings.EqualFold(alias, "alias") {
continue
}
if enabled != "" && enabled != "yes" {
continue
}
if !isOpenSUSEInstallMediaURI(uri) {
continue
}
aliases = append(aliases, alias)
}
return aliases
}
func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
listCmd := exec.CommandContext(ctx, "zypper", "repos", "-u")
output, err := listCmd.CombinedOutput()
if err != nil {
o.log(fmt.Sprintf("Warning: failed to list zypper repositories: %s", strings.TrimSpace(string(output))))
return fmt.Errorf("failed to list zypper repositories: %w", err)
}
aliases := parseZypperInstallMediaAliases(string(output))
if len(aliases) == 0 {
return nil
}
o.log(fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")))
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.055,
Step: "Disabling install media repositories...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo zypper modifyrepo -d %s", strings.Join(aliases, " ")),
LogOutput: fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")),
}
for _, alias := range aliases {
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias)))
repoOutput, err := cmd.CombinedOutput()
if err != nil {
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
return fmt.Errorf("failed to disable install media repo %s: %w", alias, err)
}
o.log(fmt.Sprintf("Disabled install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
}
return nil
}
func (o *OpenSUSEDistribution) zypperInstallArgs(packages []string, minimal bool) []string {
args := []string{"zypper", "install", "-y"}
if minimal {
args = append(args, "--no-recommends")
}
return append(args, packages...)
}
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
if len(packages) == 0 { if len(packages) == 0 {
return nil return nil
} }
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", "))) o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
groups := orderedMinimalInstallGroups(packages) args := []string{"zypper", "install", "-y"}
totalGroups := len(groups) args = append(args, packages...)
groupIndex := 0 progressChan <- InstallProgressMsg{
installGroup := func(groupPackages []string, minimal bool) error { Phase: PhaseSystemPackages,
if len(groupPackages) == 0 { Progress: 0.40,
return nil Step: "Installing system packages...",
} IsComplete: false,
NeedsSudo: true,
groupIndex++ CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
groupStart := startProgress
groupEnd := endProgress
if totalGroups > 1 {
midpoint := startProgress + ((endProgress - startProgress) / 2)
if groupIndex == 1 {
groupEnd = midpoint
} else {
groupStart = midpoint
}
}
args := o.zypperInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: groupStart,
Step: step,
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
} }
for _, group := range groups { cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
if err := installGroup(group.packages, group.minimal); err != nil { return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
return err
}
}
return nil
} }
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
+29 -51
View File
@@ -100,7 +100,9 @@ func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
} }
func (u *UbuntuDistribution) packageInstalled(pkg string) bool { func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
return debianPackageInstalledPrecisely(pkg) cmd := exec.Command("dpkg", "-l", pkg)
err := cmd.Run()
return err == nil
} }
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
@@ -452,7 +454,21 @@ func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages []
} }
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", "))) u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
args := []string{"apt-get", "install", "-y"}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
} }
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -461,59 +477,21 @@ func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []
} }
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", "))) u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing PPA packages...", 0.70, 0.85)
}
func (u *UbuntuDistribution) aptInstallArgs(packages []string, minimal bool) []string { args := []string{"apt-get", "install", "-y"}
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"} args = append(args, packages...)
if minimal {
args = append(args, "--no-install-recommends")
}
return append(args, packages...)
}
func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error { progressChan <- InstallProgressMsg{
groups := orderedMinimalInstallGroups(packages) Phase: PhaseAURPackages,
totalGroups := len(groups) Progress: 0.70,
Step: "Installing PPA packages...",
groupIndex := 0 IsComplete: false,
installGroup := func(groupPackages []string, minimal bool) error { NeedsSudo: true,
if len(groupPackages) == 0 { CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
return nil
}
groupIndex++
groupStart := startProgress
groupEnd := endProgress
if totalGroups > 1 {
midpoint := startProgress + ((endProgress - startProgress) / 2)
if groupIndex == 1 {
groupEnd = midpoint
} else {
groupStart = midpoint
}
}
args := u.aptInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: groupStart,
Step: step,
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
} }
for _, group := range groups { cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
if err := installGroup(group.packages, group.minimal); err != nil { return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
return err
}
}
return nil
} }
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -1,91 +0,0 @@
# AppArmor profile for dms-greeter
#
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
# Manual edits will be overwritten on next sync.
#
# Mode: complain (denials are logged, nothing is blocked)
# To switch to enforce after validating with `aa-logprof`:
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
#
#include <tunables/global>
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
#include <abstractions/base>
#include <abstractions/bash>
# The launcher script itself
/usr/bin/dms-greeter r,
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
/var/cache/dms-greeter/ rw,
/var/cache/dms-greeter/** rwlk,
# DMS config — packaged path
/usr/share/quickshell/dms-greeter/ r,
/usr/share/quickshell/dms-greeter/** r,
/usr/share/quickshell/ r,
/usr/share/quickshell/** r,
# DMS config — system and user overrides
/etc/dms/ r,
/etc/dms/** r,
/usr/share/dms/ r,
/usr/share/dms/** r,
/home/*/.config/quickshell/ r,
/home/*/.config/quickshell/** r,
/root/.config/quickshell/ r,
/root/.config/quickshell/** r,
# greetd / PAM — read-only for session setup
/etc/greetd/ r,
/etc/greetd/** r,
/etc/pam.d/ r,
/etc/pam.d/** r,
/usr/lib/pam.d/ r,
/usr/lib/pam.d/** r,
# Compositor binaries — run unconfined so each compositor uses its own profile
/usr/bin/niri Ux,
/usr/bin/hyprland Ux,
/usr/bin/Hyprland Ux,
/usr/bin/sway Ux,
/usr/bin/labwc Ux,
/usr/bin/scroll Ux,
/usr/bin/miracle-wm Ux,
/usr/bin/mango Ux,
# Quickshell — run unconfined (has its own compositor profile on some distros)
/usr/bin/qs Ux,
/usr/bin/quickshell Ux,
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
/run/user/[0-9]*/ rw,
/run/user/[0-9]*/** rw,
# DRM / GPU devices (required for Wayland compositor startup)
/dev/dri/ r,
/dev/dri/* rw,
/dev/udmabuf rw,
# Input devices
/dev/input/ r,
/dev/input/* r,
# Systemd journal / logging
/run/systemd/journal/socket rw,
/dev/log rw,
# Shell helper binaries invoked by the launcher script
/usr/bin/env ix,
/usr/bin/mkdir ix,
/usr/bin/cat ix,
/usr/bin/grep ix,
/usr/bin/dirname ix,
/usr/bin/basename ix,
/usr/bin/command ix,
/bin/env ix,
/bin/mkdir ix,
# Signal management (compositor lifecycle)
signal (send, receive) set=("term", "int", "hup", "kill"),
}
File diff suppressed because it is too large Load Diff
-98
View File
@@ -1,98 +0,0 @@
package greeter
import (
"os"
"path/filepath"
"testing"
)
func writeTestJSON(t *testing.T, path string, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("failed to create parent dir for %s: %v", path, err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("failed to write %s: %v", path, err)
}
}
func TestResolveGreeterThemeSyncState(t *testing.T) {
t.Parallel()
tests := []struct {
name string
settingsJSON string
sessionJSON string
wantSourcePath string
wantResolvedWallpaper string
wantDynamicOverrideUsed bool
}{
{
name: "dynamic theme with greeter wallpaper override uses generated greeter colors",
settingsJSON: `{
"currentThemeName": "dynamic",
"greeterWallpaperPath": "Pictures/blue.jpg",
"matugenScheme": "scheme-tonal-spot",
"iconTheme": "Papirus"
}`,
sessionJSON: `{"isLightMode":true}`,
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "greeter-colors", "dms-colors.json"),
wantResolvedWallpaper: filepath.Join("Pictures", "blue.jpg"),
wantDynamicOverrideUsed: true,
},
{
name: "dynamic theme without override uses desktop colors",
settingsJSON: `{
"currentThemeName": "dynamic",
"greeterWallpaperPath": ""
}`,
sessionJSON: `{"isLightMode":false}`,
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
wantResolvedWallpaper: "",
wantDynamicOverrideUsed: false,
},
{
name: "non-dynamic theme keeps desktop colors even with override wallpaper",
settingsJSON: `{
"currentThemeName": "purple",
"greeterWallpaperPath": "/tmp/blue.jpg"
}`,
sessionJSON: `{"isLightMode":false}`,
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
wantResolvedWallpaper: "/tmp/blue.jpg",
wantDynamicOverrideUsed: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
homeDir := t.TempDir()
writeTestJSON(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
writeTestJSON(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
state, err := resolveGreeterThemeSyncState(homeDir)
if err != nil {
t.Fatalf("resolveGreeterThemeSyncState returned error: %v", err)
}
if got := state.effectiveColorsSource(homeDir); got != filepath.Join(homeDir, tt.wantSourcePath) {
t.Fatalf("effectiveColorsSource = %q, want %q", got, filepath.Join(homeDir, tt.wantSourcePath))
}
wantResolvedWallpaper := tt.wantResolvedWallpaper
if wantResolvedWallpaper != "" && !filepath.IsAbs(wantResolvedWallpaper) {
wantResolvedWallpaper = filepath.Join(homeDir, wantResolvedWallpaper)
}
if state.ResolvedGreeterWallpaperPath != wantResolvedWallpaper {
t.Fatalf("ResolvedGreeterWallpaperPath = %q, want %q", state.ResolvedGreeterWallpaperPath, wantResolvedWallpaper)
}
if state.UsesDynamicWallpaperOverride != tt.wantDynamicOverrideUsed {
t.Fatalf("UsesDynamicWallpaperOverride = %v, want %v", state.UsesDynamicWallpaperOverride, tt.wantDynamicOverrideUsed)
}
})
}
}
+2 -23
View File
@@ -71,7 +71,6 @@ var templateRegistry = []TemplateDef{
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true}, {ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
{ID: "vscode", Kind: TemplateKindVSCode}, {ID: "vscode", Kind: TemplateKindVSCode},
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs}, {ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
{ID: "zed", Commands: []string{"zed", "zeditor", "zedit"}, ConfigFile: "zed.toml"},
} }
func (c *ColorMode) GTKTheme() string { func (c *ColorMode) GTKTheme() string {
@@ -99,9 +98,7 @@ type Options struct {
Mode ColorMode Mode ColorMode
IconTheme string IconTheme string
MatugenType string MatugenType string
Contrast float64
RunUserTemplates bool RunUserTemplates bool
ColorsOnly bool
StockColors string StockColors string
SyncModeWithPortal bool SyncModeWithPortal bool
TerminalsAlwaysDark bool TerminalsAlwaysDark bool
@@ -229,7 +226,6 @@ func buildOnce(opts *Options) (bool, error) {
log.Info("Running matugen color hex with stock color overrides") log.Info("Running matugen color hex with stock color overrides")
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()} args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
args = appendContrastArg(args, opts.Contrast)
args = append(args, importArgs...) args = append(args, importArgs...)
if err := runMatugen(args); err != nil { if err := runMatugen(args); err != nil {
return false, err return false, err
@@ -266,7 +262,6 @@ func buildOnce(opts *Options) (bool, error) {
args = []string{opts.Kind, opts.Value} args = []string{opts.Kind, opts.Value}
} }
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()) args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
args = appendContrastArg(args, opts.Contrast)
args = append(args, importArgs...) args = append(args, importArgs...)
if err := runMatugen(args); err != nil { if err := runMatugen(args); err != nil {
return false, err return false, err
@@ -278,10 +273,6 @@ func buildOnce(opts *Options) (bool, error) {
return false, nil return false, nil
} }
if opts.ColorsOnly {
return true, nil
}
if isDMSGTKActive(opts.ConfigDir) { if isDMSGTKActive(opts.ConfigDir) {
switch opts.Mode { switch opts.Mode {
case ColorModeLight: case ColorModeLight:
@@ -302,13 +293,6 @@ func buildOnce(opts *Options) (bool, error) {
return true, nil return true, nil
} }
func appendContrastArg(args []string, contrast float64) []string {
if contrast == 0 {
return args
}
return append(args, "--contrast", strconv.FormatFloat(contrast, 'f', -1, 64))
}
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error { func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml") userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml")
@@ -346,10 +330,6 @@ output_path = '%s'
`, opts.ShellDir, opts.ColorsOutput()) `, opts.ShellDir, opts.ColorsOutput())
if opts.ColorsOnly {
return nil
}
homeDir, _ := os.UserHomeDir() homeDir, _ := os.UserHomeDir()
for _, tmpl := range templateRegistry { for _, tmpl := range templateRegistry {
if opts.ShouldSkipTemplate(tmpl.ID) { if opts.ShouldSkipTemplate(tmpl.ID) {
@@ -616,10 +596,10 @@ func detectMatugenVersionLocked() (matugenFlags, error) {
matugenVersionOK = true matugenVersionOK = true
if matugenSupportsCOE { if matugenSupportsCOE {
log.Debugf("Matugen %s detected: continue-on-error support enabled", versionStr) log.Infof("Matugen %s supports --continue-on-error", versionStr)
} }
if matugenIsV4 { if matugenIsV4 {
log.Debugf("Matugen %s detected: using v4 compatibility flags", versionStr) log.Infof("Matugen %s: using v4 flags", versionStr)
} }
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
} }
@@ -697,7 +677,6 @@ func execDryRun(opts *Options, flags matugenFlags) (string, error) {
baseArgs = []string{opts.Kind, opts.Value} baseArgs = []string{opts.Kind, opts.Value}
} }
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run") baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
baseArgs = appendContrastArg(baseArgs, opts.Contrast)
if flags.isV4 { if flags.isV4 {
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output") baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
} }
-49
View File
@@ -3,7 +3,6 @@ package matugen
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils" mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
@@ -393,51 +392,3 @@ func TestSubstituteVars(t *testing.T) {
}) })
} }
} }
func TestBuildMergedConfigColorsOnly(t *testing.T) {
tempDir := t.TempDir()
shellDir := filepath.Join(tempDir, "shell")
configsDir := filepath.Join(shellDir, "matugen", "configs")
if err := os.MkdirAll(configsDir, 0o755); err != nil {
t.Fatalf("failed to create configs dir: %v", err)
}
baseConfig := "[config]\ncustom_keywords = []\n"
if err := os.WriteFile(filepath.Join(configsDir, "base.toml"), []byte(baseConfig), 0o644); err != nil {
t.Fatalf("failed to write base config: %v", err)
}
cfgFile, err := os.CreateTemp(tempDir, "merged-*.toml")
if err != nil {
t.Fatalf("failed to create temp config: %v", err)
}
defer os.Remove(cfgFile.Name())
defer cfgFile.Close()
opts := &Options{
ShellDir: shellDir,
ConfigDir: filepath.Join(tempDir, "config"),
StateDir: filepath.Join(tempDir, "state"),
ColorsOnly: true,
}
if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil {
t.Fatalf("buildMergedConfig failed: %v", err)
}
if err := cfgFile.Close(); err != nil {
t.Fatalf("failed to close merged config: %v", err)
}
output, err := os.ReadFile(cfgFile.Name())
if err != nil {
t.Fatalf("failed to read merged config: %v", err)
}
content := string(output)
assert.Contains(t, content, "[templates.dank]")
assert.Contains(t, content, "output_path = '"+filepath.Join(opts.StateDir, "dms-colors.json")+"'")
assert.NotContains(t, content, "[templates.gtk]")
assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs")
}
+10 -50
View File
@@ -6,20 +6,12 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/pilebones/go-udev/netlink" "github.com/pilebones/go-udev/netlink"
) )
const (
udevRecvBufSize = 8 * 1024 * 1024
udevMaxRetries = 5
udevBaseDelay = 2 * time.Second
udevMaxDelay = 60 * time.Second
)
type UdevMonitor struct { type UdevMonitor struct {
stop chan struct{} stop chan struct{}
rescanMutex sync.Mutex rescanMutex sync.Mutex
@@ -37,6 +29,13 @@ func NewUdevMonitor(manager *Manager) *UdevMonitor {
} }
func (m *UdevMonitor) run(manager *Manager) { func (m *UdevMonitor) run(manager *Manager) {
conn := &netlink.UEventConn{}
if err := conn.Connect(netlink.UdevEvent); err != nil {
log.Errorf("Failed to connect to udev netlink: %v", err)
return
}
defer conn.Close()
matcher := &netlink.RuleDefinitions{ matcher := &netlink.RuleDefinitions{
Rules: []netlink.RuleDefinition{ Rules: []netlink.RuleDefinition{
{Env: map[string]string{"SUBSYSTEM": "backlight"}}, {Env: map[string]string{"SUBSYSTEM": "backlight"}},
@@ -49,46 +48,6 @@ func (m *UdevMonitor) run(manager *Manager) {
return return
} }
failures := 0
for {
if err := m.monitorLoop(manager, matcher); err != nil {
log.Errorf("Udev monitor error: %v", err)
}
select {
case <-m.stop:
return
default:
}
failures++
if failures > udevMaxRetries {
log.Errorf("Udev monitor exceeded %d retries, giving up", udevMaxRetries)
return
}
delay := min(udevBaseDelay*time.Duration(1<<(failures-1)), udevMaxDelay)
log.Infof("Udev monitor reconnecting in %v (attempt %d/%d)", delay, failures, udevMaxRetries)
select {
case <-m.stop:
return
case <-time.After(delay):
}
}
}
func (m *UdevMonitor) monitorLoop(manager *Manager, matcher *netlink.RuleDefinitions) error {
conn := &netlink.UEventConn{}
if err := conn.Connect(netlink.UdevEvent); err != nil {
return err
}
defer conn.Close()
if err := syscall.SetsockoptInt(conn.Fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, udevRecvBufSize); err != nil {
log.Warnf("Failed to set udev socket receive buffer: %v", err)
}
events := make(chan netlink.UEvent) events := make(chan netlink.UEvent)
errs := make(chan error) errs := make(chan error)
conn.Monitor(events, errs, matcher) conn.Monitor(events, errs, matcher)
@@ -98,9 +57,10 @@ func (m *UdevMonitor) monitorLoop(manager *Manager, matcher *netlink.RuleDefinit
for { for {
select { select {
case <-m.stop: case <-m.stop:
return nil return
case err := <-errs: case err := <-errs:
return err log.Errorf("Udev monitor error: %v", err)
return
case event := <-events: case event := <-events:
m.handleEvent(manager, event) m.handleEvent(manager, event)
} }
-1
View File
@@ -29,7 +29,6 @@ func handleMatugenQueue(conn net.Conn, req models.Request) {
SyncModeWithPortal: models.GetOr(req, "syncModeWithPortal", false), SyncModeWithPortal: models.GetOr(req, "syncModeWithPortal", false),
TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false), TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false),
SkipTemplates: models.GetOr(req, "skipTemplates", ""), SkipTemplates: models.GetOr(req, "skipTemplates", ""),
Contrast: models.GetOr(req, "contrast", 0.0),
} }
wait := models.GetOr(req, "wait", true) wait := models.GetOr(req, "wait", true)
+2 -9
View File
@@ -1,7 +1,6 @@
package network package network
import ( import (
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -29,13 +28,7 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
func TestDetectNetworkStack_Integration(t *testing.T) { func TestDetectNetworkStack_Integration(t *testing.T) {
result, err := DetectNetworkStack() result, err := DetectNetworkStack()
if err != nil && strings.Contains(err.Error(), "connect system bus") {
t.Skipf("system D-Bus unavailable: %v", err)
}
assert.NoError(t, err) assert.NoError(t, err)
if assert.NotNil(t, result) { assert.NotNil(t, result)
assert.NotEmpty(t, result.ChosenReason) assert.NotEmpty(t, result.ChosenReason)
}
} }
+27 -40
View File
@@ -73,7 +73,6 @@ var dbusManager *serverDbus.Manager
var wlContext *wlcontext.SharedContext var wlContext *wlcontext.SharedContext
var themeModeManager *thememode.Manager var themeModeManager *thememode.Manager
var locationManager *location.Manager var locationManager *location.Manager
var geoClientInstance geolocation.Client
const dbusClientID = "dms-dbus-client" const dbusClientID = "dms-dbus-client"
@@ -192,7 +191,7 @@ func InitializeFreedeskManager() error {
return nil return nil
} }
func InitializeWaylandManager() error { func InitializeWaylandManager(geoClient geolocation.Client) error {
log.Info("Attempting to initialize Wayland gamma control...") log.Info("Attempting to initialize Wayland gamma control...")
if wlContext == nil { if wlContext == nil {
@@ -205,7 +204,7 @@ func InitializeWaylandManager() error {
} }
config := wayland.DefaultConfig() config := wayland.DefaultConfig()
manager, err := wayland.NewManager(wlContext.Display(), config) manager, err := wayland.NewManager(wlContext.Display(), geoClient, config)
if err != nil { if err != nil {
log.Errorf("Failed to initialize wayland manager: %v", err) log.Errorf("Failed to initialize wayland manager: %v", err)
return err return err
@@ -386,8 +385,8 @@ func InitializeDbusManager() error {
return nil return nil
} }
func InitializeThemeModeManager() error { func InitializeThemeModeManager(geoClient geolocation.Client) error {
manager := thememode.NewManager() manager := thememode.NewManager(geoClient)
themeModeManager = manager themeModeManager = manager
log.Info("Theme mode automation manager initialized") log.Info("Theme mode automation manager initialized")
@@ -1331,9 +1330,6 @@ func cleanupManagers() {
if locationManager != nil { if locationManager != nil {
locationManager.Close() locationManager.Close()
} }
if geoClientInstance != nil {
geoClientInstance.Close()
}
} }
func Start(printDocs bool) error { func Start(printDocs bool) error {
@@ -1549,6 +1545,9 @@ func Start(printDocs bool) error {
loginctlReady := make(chan struct{}) loginctlReady := make(chan struct{})
freedesktopReady := make(chan struct{}) freedesktopReady := make(chan struct{})
geoClient := geolocation.NewClient()
defer geoClient.Close()
go func() { go func() {
defer close(loginctlReady) defer close(loginctlReady)
if err := InitializeLoginctlManager(); err != nil { if err := InitializeLoginctlManager(); err != nil {
@@ -1593,41 +1592,10 @@ func Start(printDocs bool) error {
} }
}() }()
if err := InitializeWaylandManager(); err != nil { if err := InitializeWaylandManager(geoClient); err != nil {
log.Warnf("Wayland manager unavailable: %v", err) log.Warnf("Wayland manager unavailable: %v", err)
} }
if err := InitializeThemeModeManager(); err != nil {
log.Warnf("Theme mode manager unavailable: %v", err)
} else {
notifyCapabilityChange()
go func() {
<-loginctlReady
if loginctlManager == nil {
return
}
themeModeManager.WatchLoginctl(loginctlManager)
}()
}
go func() {
geoClient := geolocation.NewClient()
geoClientInstance = geoClient
if waylandManager != nil {
waylandManager.SetGeoClient(geoClient)
}
if themeModeManager != nil {
themeModeManager.SetGeoClient(geoClient)
}
if err := InitializeLocationManager(geoClient); err != nil {
log.Warnf("Location manager unavailable: %v", err)
} else {
notifyCapabilityChange()
}
}()
go func() { go func() {
if err := InitializeBluezManager(); err != nil { if err := InitializeBluezManager(); err != nil {
log.Warnf("Bluez manager unavailable: %v", err) log.Warnf("Bluez manager unavailable: %v", err)
@@ -1656,6 +1624,25 @@ func Start(printDocs bool) error {
log.Debugf("WlrOutput manager unavailable: %v", err) log.Debugf("WlrOutput manager unavailable: %v", err)
} }
if err := InitializeThemeModeManager(geoClient); err != nil {
log.Warnf("Theme mode manager unavailable: %v", err)
} else {
notifyCapabilityChange()
go func() {
<-loginctlReady
if loginctlManager == nil {
return
}
themeModeManager.WatchLoginctl(loginctlManager)
}()
}
if err := InitializeLocationManager(geoClient); err != nil {
log.Warnf("Location 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() {
+2 -8
View File
@@ -40,7 +40,7 @@ type Manager struct {
wg sync.WaitGroup wg sync.WaitGroup
} }
func NewManager() *Manager { func NewManager(geoClient geolocation.Client) *Manager {
m := &Manager{ m := &Manager{
config: Config{ config: Config{
Enabled: false, Enabled: false,
@@ -54,6 +54,7 @@ func NewManager() *Manager {
}, },
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
updateTrigger: make(chan struct{}, 1), updateTrigger: make(chan struct{}, 1),
geoClient: geoClient,
} }
m.updateState(time.Now()) m.updateState(time.Now())
@@ -314,10 +315,6 @@ func (m *Manager) getConfig() Config {
return m.config return m.config
} }
func (m *Manager) SetGeoClient(client geolocation.Client) {
m.geoClient = client
}
func (m *Manager) getLocation(config Config) (*float64, *float64) { func (m *Manager) getLocation(config Config) (*float64, *float64) {
if config.Latitude != nil && config.Longitude != nil { if config.Latitude != nil && config.Longitude != nil {
return config.Latitude, config.Longitude return config.Latitude, config.Longitude
@@ -325,9 +322,6 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
if !config.UseIPLocation { if !config.UseIPLocation {
return nil, nil return nil, nil
} }
if m.geoClient == nil {
return nil, nil
}
m.locationMutex.RLock() m.locationMutex.RLock()
if m.cachedIPLat != nil && m.cachedIPLon != nil { if m.cachedIPLat != nil && m.cachedIPLon != nil {
+20 -27
View File
@@ -20,7 +20,7 @@ import (
const animKelvinStep = 25 const animKelvinStep = 25
func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) { func NewManager(display wlclient.WaylandDisplay, geoClient geolocation.Client, config Config) (*Manager, error) {
if err := config.Validate(); err != nil { if err := config.Validate(); err != nil {
return nil, err return nil, err
} }
@@ -41,6 +41,7 @@ func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error
updateTrigger: make(chan struct{}, 1), updateTrigger: make(chan struct{}, 1),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
dbusSignal: make(chan *dbus.Signal, 16), dbusSignal: make(chan *dbus.Signal, 16),
geoClient: geoClient,
} }
if err := m.setupRegistry(); err != nil { if err := m.setupRegistry(); err != nil {
@@ -421,10 +422,6 @@ func (m *Manager) recalcSchedule(now time.Time) {
} }
} }
func (m *Manager) SetGeoClient(client geolocation.Client) {
m.geoClient = client
}
func (m *Manager) getLocation() (*float64, *float64) { func (m *Manager) getLocation() (*float64, *float64) {
m.configMutex.RLock() m.configMutex.RLock()
config := m.config config := m.config
@@ -433,31 +430,27 @@ func (m *Manager) getLocation() (*float64, *float64) {
if config.Latitude != nil && config.Longitude != nil { if config.Latitude != nil && config.Longitude != nil {
return config.Latitude, config.Longitude return config.Latitude, config.Longitude
} }
if !config.UseIPLocation { if config.UseIPLocation {
return nil, nil m.locationMutex.RLock()
} if m.cachedIPLat != nil && m.cachedIPLon != nil {
if m.geoClient == nil { lat, lon := m.cachedIPLat, m.cachedIPLon
return nil, nil m.locationMutex.RUnlock()
} return lat, lon
}
m.locationMutex.RLock()
if m.cachedIPLat != nil && m.cachedIPLon != nil {
lat, lon := m.cachedIPLat, m.cachedIPLon
m.locationMutex.RUnlock() m.locationMutex.RUnlock()
return lat, lon
}
m.locationMutex.RUnlock()
location, err := m.geoClient.GetLocation() location, err := m.geoClient.GetLocation()
if err != nil { if err != nil {
return nil, nil return nil, nil
} }
m.locationMutex.Lock() m.locationMutex.Lock()
m.cachedIPLat = &location.Latitude m.cachedIPLat = &location.Latitude
m.cachedIPLon = &location.Longitude m.cachedIPLon = &location.Longitude
m.locationMutex.Unlock() m.locationMutex.Unlock()
return m.cachedIPLat, m.cachedIPLon return m.cachedIPLat, m.cachedIPLon
}
return nil, nil
} }
func (m *Manager) hasValidSchedule() bool { func (m *Manager) hasValidSchedule() bool {
+5 -2
View File
@@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
mocks_geolocation "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/geolocation"
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient" mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
) )
@@ -390,18 +391,20 @@ func TestNotifySubscribers_NonBlocking(t *testing.T) {
func TestNewManager_GetRegistryError(t *testing.T) { func TestNewManager_GetRegistryError(t *testing.T) {
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
mockGeoclient := mocks_geolocation.NewMockClient(t)
mockDisplay.EXPECT().Context().Return(nil) mockDisplay.EXPECT().Context().Return(nil)
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry")) mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
config := DefaultConfig() config := DefaultConfig()
_, err := NewManager(mockDisplay, config) _, err := NewManager(mockDisplay, mockGeoclient, config)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "get registry") assert.Contains(t, err.Error(), "get registry")
} }
func TestNewManager_InvalidConfig(t *testing.T) { func TestNewManager_InvalidConfig(t *testing.T) {
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
mockGeoclient := mocks_geolocation.NewMockClient(t)
config := Config{ config := Config{
LowTemp: 500, LowTemp: 500,
@@ -409,6 +412,6 @@ func TestNewManager_InvalidConfig(t *testing.T) {
Gamma: 1.0, Gamma: 1.0,
} }
_, err := NewManager(mockDisplay, config) _, err := NewManager(mockDisplay, mockGeoclient, config)
assert.Error(t, err) assert.Error(t, err)
} }
+4 -17
View File
@@ -2,10 +2,10 @@ package wlcontext
import ( import (
"fmt" "fmt"
"golang.org/x/sys/unix"
"os" "os"
"sync" "sync"
"time"
"golang.org/x/sys/unix"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -123,9 +123,6 @@ func (sc *SharedContext) eventDispatcher() {
{Fd: int32(sc.wakeR), Events: unix.POLLIN}, {Fd: int32(sc.wakeR), Events: unix.POLLIN},
} }
consecutiveErrors := 0
const maxConsecutiveErrors = 20
for { for {
sc.drainCmdQueue() sc.drainCmdQueue()
@@ -156,19 +153,9 @@ func (sc *SharedContext) eventDispatcher() {
} }
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) { if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
consecutiveErrors++ log.Errorf("Wayland connection error: %v", err)
log.Warnf("Wayland connection error (%d/%d): %v", consecutiveErrors, maxConsecutiveErrors, err) return
if consecutiveErrors >= maxConsecutiveErrors {
log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors)
return
}
time.Sleep(100 * time.Millisecond * time.Duration(consecutiveErrors))
continue
} }
consecutiveErrors = 0
} }
} }
+1 -1
View File
@@ -40,7 +40,7 @@ func (m Model) viewDeployingConfigs() string {
spinner := m.spinner.View() spinner := m.spinner.View()
status := m.styles.Normal.Render("Setting up configuration files...") status := m.styles.Normal.Render("Setting up configuration files...")
fmt.Fprintf(&b, "%s %s", spinner, status) b.WriteString(fmt.Sprintf("%s %s", spinner, status))
b.WriteString("\n\n") b.WriteString("\n\n")
// Show progress information // Show progress information
+1 -1
View File
@@ -23,7 +23,7 @@ func (m Model) viewDetectingDeps() string {
spinner := m.spinner.View() spinner := m.spinner.View()
status := m.styles.Normal.Render("Scanning system for existing packages and configurations...") status := m.styles.Normal.Render("Scanning system for existing packages and configurations...")
fmt.Fprintf(&b, "%s %s", spinner, status) b.WriteString(fmt.Sprintf("%s %s", spinner, status))
return b.String() return b.String()
} }
+2 -2
View File
@@ -52,7 +52,7 @@ func (m Model) viewInstallingPackages() string {
if !m.packageProgress.isComplete { if !m.packageProgress.isComplete {
spinner := m.spinner.View() spinner := m.spinner.View()
status := m.styles.Normal.Render(m.packageProgress.step) status := m.styles.Normal.Render(m.packageProgress.step)
fmt.Fprintf(&b, "%s %s", spinner, status) b.WriteString(fmt.Sprintf("%s %s", spinner, status))
b.WriteString("\n\n") b.WriteString("\n\n")
// Show progress bar // Show progress bar
@@ -387,7 +387,7 @@ func (m Model) viewDebugLogs() string {
for i := startIdx; i < len(allLogs); i++ { for i := startIdx; i < len(allLogs); i++ {
if allLogs[i] != "" { if allLogs[i] != "" {
fmt.Fprintf(&b, "%d: %s\n", i, allLogs[i]) b.WriteString(fmt.Sprintf("%d: %s\n", i, allLogs[i]))
} }
} }
+1 -1
View File
@@ -75,7 +75,7 @@ func (m Model) viewFingerprintAuth() string {
spinner := m.spinner.View() spinner := m.spinner.View()
status := m.styles.Normal.Render("Waiting for fingerprint...") status := m.styles.Normal.Render("Waiting for fingerprint...")
fmt.Fprintf(&b, "%s %s", spinner, status) b.WriteString(fmt.Sprintf("%s %s", spinner, status))
} }
return b.String() return b.String()
+3 -3
View File
@@ -132,9 +132,9 @@ func (m Model) viewWelcome() string {
contentStyle = contentStyle.Bold(true) contentStyle = contentStyle.Bold(true)
} }
fmt.Fprintf(&b, " %s %s\n", b.WriteString(fmt.Sprintf(" %s %s\n",
prefixStyle.Render(prefix), prefixStyle.Render(prefix),
contentStyle.Render(content)) contentStyle.Render(content)))
} }
b.WriteString("\n") b.WriteString("\n")
@@ -158,7 +158,7 @@ func (m Model) viewWelcome() string {
} else if m.isLoading { } else if m.isLoading {
spinner := m.spinner.View() spinner := m.spinner.View()
loading := m.styles.Normal.Render("Detecting system...") loading := m.styles.Normal.Render("Detecting system...")
fmt.Fprintf(&b, "%s %s\n\n", spinner, loading) b.WriteString(fmt.Sprintf("%s %s\n\n", spinner, loading))
} }
// Footer with better visual separation // Footer with better visual separation
+1 -1
View File
@@ -13,7 +13,7 @@ Architecture: any
Depends: ${misc:Depends}, Depends: ${misc:Depends},
greetd, greetd,
quickshell-git | quickshell quickshell-git | quickshell
Suggests: niri | hyprland | sway Recommends: niri | hyprland | sway
Description: DankMaterialShell greeter for greetd Description: DankMaterialShell greeter for greetd
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3 DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors. inspired greeter interface built with Quickshell for Wayland compositors.
+1 -1
View File
@@ -9,7 +9,7 @@ Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
Package: dms Package: dms
Architecture: amd64 arm64 Architecture: amd64
Depends: ${misc:Depends}, Depends: ${misc:Depends},
quickshell | quickshell-git, quickshell | quickshell-git,
accountsservice, accountsservice,
@@ -1,3 +1,2 @@
dms-distropkg-amd64.gz dms-distropkg-amd64.gz
dms-distropkg-arm64.gz
dms-source.tar.gz dms-source.tar.gz
-1
View File
@@ -1,5 +1,4 @@
# Include files that are normally excluded by .gitignore # Include files that are normally excluded by .gitignore
# These are needed for the build process on Launchpad # These are needed for the build process on Launchpad
tar-ignore = !dms-distropkg-amd64.gz tar-ignore = !dms-distropkg-amd64.gz
tar-ignore = !dms-distropkg-arm64.gz
tar-ignore = !dms-source.tar.gz tar-ignore = !dms-source.tar.gz
+1 -1
View File
@@ -3,7 +3,7 @@
%global debug_package %{nil} %global debug_package %{nil}
%global version {{{ git_repo_version }}} %global version {{{ git_repo_version }}}
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors %global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
%global go_toolchain_version 1.26.1 %global go_toolchain_version 1.25.7
Name: dms Name: dms
Epoch: 2 Epoch: 2
+1 -4
View File
@@ -24,7 +24,6 @@ let
lib.makeBinPath [ lib.makeBinPath [
cfg.quickshell.package cfg.quickshell.package
compositorPackage compositorPackage
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
] ]
} }
${ ${
@@ -196,9 +195,7 @@ in
fi fi
if [ -f settings.json ]; then if [ -f settings.json ]; then
theme_file="$(${jq} -r '.customThemeFile // empty' settings.json)" if cp "$(${jq} -r '.customThemeFile' settings.json)" custom-theme.json; then
if [ -f "$theme_file" ] && [ -r "$theme_file" ]; then
cp "$theme_file" custom-theme.json
mv settings.json settings.orig.json mv settings.json settings.orig.json
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json ${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
fi fi
+26 -23
View File
@@ -2,9 +2,11 @@
config, config,
lib, lib,
... ...
}: let }:
let
cfg = config.programs.dank-material-shell; cfg = config.programs.dank-material-shell;
in { in
{
imports = [ imports = [
./dms-rename.nix ./dms-rename.nix
]; ];
@@ -14,11 +16,9 @@ in {
enableKeybinds = lib.mkEnableOption "DankMaterialShell niri keybinds"; enableKeybinds = lib.mkEnableOption "DankMaterialShell niri keybinds";
enableSpawn = lib.mkEnableOption "DankMaterialShell niri spawn-at-startup"; enableSpawn = lib.mkEnableOption "DankMaterialShell niri spawn-at-startup";
includes = { includes = {
enable = enable = (lib.mkEnableOption "includes for niri-flake") // {
(lib.mkEnableOption "includes for niri-flake") default = true;
// { };
default = true;
};
override = lib.mkOption { override = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
description = '' description = ''
@@ -44,10 +44,8 @@ in {
"alttab" "alttab"
"binds" "binds"
"colors" "colors"
"cursor"
"layout" "layout"
"outputs" "outputs"
"windowrules"
"wpblur" "wpblur"
]; ];
example = [ example = [
@@ -72,21 +70,24 @@ in {
let let
cfg' = cfg.niri.includes; cfg' = cfg.niri.includes;
withOriginalConfig = dmsFiles: withOriginalConfig =
if cfg'.override dmsFiles:
then [cfg'.originalFileName] ++ dmsFiles if cfg'.override then
else dmsFiles ++ [cfg'.originalFileName]; [ cfg'.originalFileName ] ++ dmsFiles
else
dmsFiles ++ [ cfg'.originalFileName ];
fixes = map (fix: "\n${fix}") ( fixes = map (fix: "\n${fix}") (
lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable) lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable)
# kdl # kdl
'' ''
// Border fix // Border fix
// See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details // See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details
layout { border { on; }; } layout { border { on; }; }
'' ''
); );
in { in
{
niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl"; niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl";
niri-config-dms = { niri-config-dms = {
target = "niri/config.kdl"; target = "niri/config.kdl";
@@ -103,9 +104,11 @@ in {
programs.niri.settings = lib.mkMerge [ programs.niri.settings = lib.mkMerge [
(lib.mkIf cfg.niri.enableKeybinds { (lib.mkIf cfg.niri.enableKeybinds {
binds = with config.lib.niri.actions; let binds =
dms-ipc = spawn "dms" "ipc"; with config.lib.niri.actions;
in let
dms-ipc = spawn "dms" "ipc";
in
{ {
"Mod+Space" = { "Mod+Space" = {
action = dms-ipc "spotlight" "toggle"; action = dms-ipc "spotlight" "toggle";
+3 -31
View File
@@ -102,19 +102,6 @@ if [[ ! -d "distro/debian" ]]; then
echo "Error: Run this script from the repository root" echo "Error: Run this script from the repository root"
exit 1 exit 1
fi fi
# Retry wrapper for osc commands (mitigates SSL "Connection reset by peer" from api.opensuse.org)
osc_retry() {
local max=3 attempt=1
while true; do
if osc "$@"; then return 0; fi
((attempt >= max)) && return 1
echo "Retrying in $((5*attempt))s (attempt $attempt/$max)..."
sleep $((5*attempt))
((attempt++))
done
}
# Parameters: # Parameters:
# $1 = PROJECT # $1 = PROJECT
# $2 = PACKAGE # $2 = PACKAGE
@@ -322,23 +309,8 @@ mkdir -p "$OBS_BASE"
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
echo "Checking out $OBS_PROJECT/$PACKAGE..." echo "Checking out $OBS_PROJECT/$PACKAGE..."
cd "$OBS_BASE" cd "$OBS_BASE"
CHECKOUT_OK=false osc co "$OBS_PROJECT/$PACKAGE"
for attempt in 1 2 3; do
if osc co "$OBS_PROJECT/$PACKAGE"; then
CHECKOUT_OK=true
break
fi
if [[ $attempt -lt 3 ]]; then
echo "Checkout failed (attempt $attempt/3). Removing partial copy and retrying in $((5*attempt))s..."
rm -rf "${OBS_BASE:?}/${OBS_PROJECT:?}"
sleep $((5*attempt))
fi
done
cd "$REPO_ROOT" cd "$REPO_ROOT"
if [[ "$CHECKOUT_OK" != "true" ]]; then
echo "Error: Checkout failed after 3 attempts"
exit 1
fi
fi fi
WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE" WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE"
@@ -1092,7 +1064,7 @@ fi
# Update working copy to latest revision (without expanding service files to avoid revision conflicts) # Update working copy to latest revision (without expanding service files to avoid revision conflicts)
echo "==> Updating working copy" echo "==> Updating working copy"
if ! osc_retry up 2>/dev/null; then if ! osc up 2>/dev/null; then
echo "Error: Failed to update working copy" echo "Error: Failed to update working copy"
exit 1 exit 1
fi fi
@@ -1173,7 +1145,7 @@ if ! osc status 2>/dev/null | grep -qE '^[MAD]|^[?]'; then
else else
echo "==> Committing to OBS" echo "==> Committing to OBS"
set +e set +e
osc_retry commit --skip-local-service-run -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs" osc commit --skip-local-service-run -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs"
COMMIT_EXIT=${PIPESTATUS[0]} COMMIT_EXIT=${PIPESTATUS[0]}
set -e set -e
if [[ $COMMIT_EXIT -ne 0 ]]; then if [[ $COMMIT_EXIT -ne 0 ]]; then
+1 -1
View File
@@ -13,7 +13,7 @@ Architecture: any
Depends: ${misc:Depends}, Depends: ${misc:Depends},
greetd, greetd,
quickshell-git | quickshell quickshell-git | quickshell
Suggests: niri | hyprland | sway Recommends: niri | hyprland | sway
Description: DankMaterialShell greeter for greetd Description: DankMaterialShell greeter for greetd
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3 DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors. inspired greeter interface built with Quickshell for Wayland compositors.
-2
View File
@@ -538,8 +538,6 @@ Color picker modal control.
**Functions:** **Functions:**
- `open` - Show color picker modal - `open` - Show color picker modal
- `openColor <color>` - Show color picker modal with a pre-selected color
- Parameters: `color` - Color string (e.g. "#ff0000", "#3f51b5")
- `close` - Hide color picker modal - `close` - Hide color picker modal
- `closeInstant` - Hide color picker modal without animation - `closeInstant` - Hide color picker modal without animation
- `toggle` - Toggle color picker modal visibility - `toggle` - Toggle color picker modal visibility
+57 -82
View File
@@ -17,25 +17,6 @@
... ...
}: }:
let let
goModVersion =
let
content = builtins.readFile ./core/go.mod;
lines = builtins.filter builtins.isString (builtins.split "\n" content);
goLines = builtins.filter (l: builtins.match "go [0-9]+\\..*" l != null) lines;
matched =
if goLines != [ ] then builtins.match "go ([0-9]+)\\.([0-9]+).*" (builtins.head goLines) else null;
in
if matched != null then
{
major = builtins.elemAt matched 0;
minor = builtins.elemAt matched 1;
}
else
{
major = "1";
minor = "25";
};
goForPkgs = pkgs: pkgs.${"go_${goModVersion.major}_${goModVersion.minor}"};
forEachSystem = forEachSystem =
fn: fn:
nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] ( nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] (
@@ -91,82 +72,76 @@
"${cleanVersion}${dateSuffix}${revSuffix}"; "${cleanVersion}${dateSuffix}${revSuffix}";
in in
{ {
dms-shell = pkgs.lib.makeOverridable ( dms-shell = pkgs.buildGoModule (
let
rootSrc = ./.;
in
{ {
extraQtPackages ? [ ], inherit version;
}: pname = "dms-shell";
(pkgs.buildGoModule.override { go = goForPkgs pkgs; }) ( src = ./core;
let vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
rootSrc = ./.;
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
in
{
inherit version;
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
subPackages = [ "cmd/dms" ]; subPackages = [ "cmd/dms" ];
ldflags = [ ldflags = [
"-s" "-s"
"-w" "-w"
"-X 'main.Version=${version}'" "-X 'main.Version=${version}'"
]; ];
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
installShellFiles installShellFiles
makeWrapper makeWrapper
]; ];
postInstall = '' postInstall = ''
mkdir -p $out/share/quickshell/dms mkdir -p $out/share/quickshell/dms
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/ cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
chmod u+w $out/share/quickshell/dms/VERSION chmod u+w $out/share/quickshell/dms/VERSION
echo "${version}" > $out/share/quickshell/dms/VERSION echo "${version}" > $out/share/quickshell/dms/VERSION
# Install desktop file and icon # Install desktop file and icon
install -D ${rootSrc}/assets/dms-open.desktop \ install -D ${rootSrc}/assets/dms-open.desktop \
$out/share/applications/dms-open.desktop $out/share/applications/dms-open.desktop
install -D ${rootSrc}/core/assets/danklogo.svg \ install -D ${rootSrc}/core/assets/danklogo.svg \
$out/share/hicolor/scalable/apps/danklogo.svg $out/share/hicolor/scalable/apps/danklogo.svg
wrapProgram $out/bin/dms \ wrapProgram $out/bin/dms \
--add-flags "-c $out/share/quickshell/dms" \ --add-flags "-c $out/share/quickshell/dms" \
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \ --prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}" \
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}" --prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs (qmlPkgs pkgs)}"
install -Dm644 ${rootSrc}/assets/systemd/dms.service \ install -Dm644 ${rootSrc}/assets/systemd/dms.service \
$out/lib/systemd/user/dms.service $out/lib/systemd/user/dms.service
substituteInPlace $out/lib/systemd/user/dms.service \ substituteInPlace $out/lib/systemd/user/dms.service \
--replace-fail /usr/bin/dms $out/bin/dms \ --replace-fail /usr/bin/dms $out/bin/dms \
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill --replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \ substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash --replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \ substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so --replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
installShellCompletion --cmd dms \ installShellCompletion --cmd dms \
--bash <($out/bin/dms completion bash) \ --bash <($out/bin/dms completion bash) \
--fish <($out/bin/dms completion fish) \ --fish <($out/bin/dms completion fish) \
--zsh <($out/bin/dms completion zsh) --zsh <($out/bin/dms completion zsh)
''; '';
meta = { meta = {
description = "Desktop shell for wayland compositors built with Quickshell & GO"; description = "Desktop shell for wayland compositors built with Quickshell & GO";
homepage = "https://danklinux.com"; homepage = "https://danklinux.com";
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}"; changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
license = pkgs.lib.licenses.mit; license = pkgs.lib.licenses.mit;
mainProgram = "dms"; mainProgram = "dms";
platforms = pkgs.lib.platforms.linux; platforms = pkgs.lib.platforms.linux;
}; };
} }
) );
) { };
quickshell = quickshell.packages.${system}.default; quickshell = quickshell.packages.${system}.default;
@@ -206,7 +181,7 @@
buildInputs = buildInputs =
with pkgs; with pkgs;
[ [
(goForPkgs pkgs) go_1_25
go-mockery_2 go-mockery_2
gopls gopls
delve delve
+2 -2
View File
@@ -99,7 +99,7 @@ qs -v -p shell.qml # Verbose debugging
# Code formatting and linting # Code formatting and linting
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format QML (don't use qmlformat) qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format QML (don't use qmlformat)
make -C .. lint-qml # From quickshell/, call the repo-root lint target; requires the generated .qmlls.ini VFS from `qs -p .` qmllint **/*.qml # Lint all QML files
./qmlformat-all.sh # Format all QML files ./qmlformat-all.sh # Format all QML files
``` ```
@@ -783,7 +783,7 @@ When modifying the shell:
**QML Frontend:** **QML Frontend:**
1. **Test changes**: `qs -p .` (automatic reload on file changes) 1. **Test changes**: `qs -p .` (automatic reload on file changes)
2. **Code quality**: Run `./qmlformat-all.sh` or `qmlformat -i **/*.qml`, then from repo root run `make lint-qml` after Quickshell has generated the local `.qmlls.ini` VFS with `qs -p .` 2. **Code quality**: Run `./qmlformat-all.sh` or `qmlformat -i **/*.qml` and `qmllint **/*.qml`
3. **Performance**: Ensure animations remain smooth (60 FPS target) 3. **Performance**: Ensure animations remain smooth (60 FPS target)
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency 4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
+174
View File
@@ -0,0 +1,174 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
// AnimVariants Central tuning for animation and Motion Effects variants
// (Material/Fluent/Dynamic) (Standard/Directional/Depth)
Singleton {
id: root
readonly property list<real> variantEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
switch (SettingsData.animationVariant) {
case 1:
return Anims.standardDecel;
case 2:
return Anims.expressiveFastSpatial;
default:
return Anims.expressiveDefaultSpatial;
}
}
readonly property list<real> variantExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
switch (SettingsData.animationVariant) {
case 1:
return Anims.standard;
case 2:
return Anims.emphasized;
default:
return Anims.emphasized;
}
}
// Modal-specific entry curve
readonly property list<real> variantModalEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.standardDecel;
if (SettingsData.animationVariant === 2)
return Anims.expressiveFastSpatial;
}
return variantEnterCurve;
}
readonly property list<real> variantModalExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.emphasizedAccel;
if (SettingsData.animationVariant === 2)
return Anims.emphasizedAccel;
}
return variantExitCurve;
}
// Popout-specific entry curve
readonly property list<real> variantPopoutEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.standardDecel;
if (SettingsData.animationVariant === 2)
return Anims.expressiveFastSpatial;
return Anims.standardDecel;
}
return variantEnterCurve;
}
readonly property list<real> variantPopoutExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.emphasizedAccel;
if (SettingsData.animationVariant === 2)
return Anims.emphasizedAccel;
}
return variantExitCurve;
}
readonly property real variantEnterDurationFactor: {
if (typeof SettingsData === "undefined")
return 1.0;
switch (SettingsData.animationVariant) {
case 1:
return 0.9;
case 2:
return 1.08;
default:
return 1.0;
}
}
readonly property real variantExitDurationFactor: {
if (typeof SettingsData === "undefined")
return 1.0;
switch (SettingsData.animationVariant) {
case 1:
return 0.85;
case 2:
return 0.92;
default:
return 1.0;
}
}
// Fluent: opacity at ~55% of duration; Material/Dynamic: 1:1 with position
readonly property real variantOpacityDurationScale: {
if (typeof SettingsData === "undefined")
return 1.0;
return SettingsData.animationVariant === 1 ? 0.55 : 1.0;
}
function variantDuration(baseDuration, entering) {
const factor = entering ? variantEnterDurationFactor : variantExitDurationFactor;
return Math.max(0, Math.round(baseDuration * factor));
}
function variantExitCleanupPadding() {
if (typeof SettingsData === "undefined")
return 50;
switch (SettingsData.motionEffect) {
case 1:
return 8;
case 2:
return 24;
default:
return 50;
}
}
function variantCloseInterval(baseDuration) {
return variantDuration(baseDuration, false) + variantExitCleanupPadding();
}
readonly property bool isDirectionalEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 1
readonly property bool isDepthEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 2
readonly property real effectScaleCollapsed: {
if (typeof SettingsData === "undefined")
return 0.96;
switch (SettingsData.motionEffect) {
case 1:
return 1.0;
case 2:
return 0.88;
default:
return 0.96;
}
}
readonly property real effectAnimOffset: {
if (typeof SettingsData === "undefined")
return 16;
switch (SettingsData.motionEffect) {
case 1:
return 144;
case 2:
return 56;
default:
return 16;
}
}
}
+5
View File
@@ -22,4 +22,9 @@ Singleton {
readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00] readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00] readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00] readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00]
// Used by AnimVariants for variant/effect logic
readonly property var expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
readonly property var expressiveFastSpatial: [0.34, 1.5, 0.2, 1.0, 1.0, 1.0]
readonly property var expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
} }
+36 -16
View File
@@ -22,8 +22,8 @@ Singleton {
property bool _hasUnsavedChanges: false property bool _hasUnsavedChanges: false
property var _loadedSessionSnapshot: null property var _loadedSessionSnapshot: null
readonly property var _hooks: ({ readonly property var _hooks: ({
"updateLocale": updateLocale "updateLocale": updateLocale
}) })
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
readonly property string _stateDir: Paths.strip(_stateUrl) readonly property string _stateDir: Paths.strip(_stateUrl)
@@ -134,8 +134,6 @@ Singleton {
property string launcherLastMode: "all" property string launcherLastMode: "all"
property string appDrawerLastMode: "apps" property string appDrawerLastMode: "apps"
property string niriOverviewLastMode: "apps" property string niriOverviewLastMode: "apps"
property string settingsSidebarExpandedIds: ","
property string settingsSidebarCollapsedIds: ","
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) { if (!isGreeterMode) {
@@ -582,7 +580,14 @@ Singleton {
} }
} }
newSettings[identifier] = getMonitorCyclingSettings(screenName); if (!newSettings[identifier]) {
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].enabled = enabled; newSettings[identifier].enabled = enabled;
monitorCyclingSettings = newSettings; monitorCyclingSettings = newSettings;
saveSettings(); saveSettings();
@@ -613,7 +618,14 @@ Singleton {
} }
} }
newSettings[identifier] = getMonitorCyclingSettings(screenName); if (!newSettings[identifier]) {
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].mode = mode; newSettings[identifier].mode = mode;
monitorCyclingSettings = newSettings; monitorCyclingSettings = newSettings;
saveSettings(); saveSettings();
@@ -644,7 +656,14 @@ Singleton {
} }
} }
newSettings[identifier] = getMonitorCyclingSettings(screenName); if (!newSettings[identifier]) {
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].interval = interval; newSettings[identifier].interval = interval;
monitorCyclingSettings = newSettings; monitorCyclingSettings = newSettings;
saveSettings(); saveSettings();
@@ -675,7 +694,14 @@ Singleton {
} }
} }
newSettings[identifier] = getMonitorCyclingSettings(screenName); if (!newSettings[identifier]) {
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].time = time; newSettings[identifier].time = time;
monitorCyclingSettings = newSettings; monitorCyclingSettings = newSettings;
saveSettings(); saveSettings();
@@ -1106,12 +1132,6 @@ Singleton {
saveSettings(); saveSettings();
} }
function setSettingsSidebarState(expandedIds, collapsedIds) {
settingsSidebarExpandedIds = expandedIds;
settingsSidebarCollapsedIds = collapsedIds;
saveSettings();
}
function syncWallpaperForCurrentMode() { function syncWallpaperForCurrentMode() {
if (!perModeWallpaper) if (!perModeWallpaper)
return; return;
@@ -1198,7 +1218,7 @@ Singleton {
"time": "06:00" "time": "06:00"
}; };
var value = _findMonitorValue(monitorCyclingSettings, screenName); var value = _findMonitorValue(monitorCyclingSettings, screenName);
return Object.assign({}, defaults, value !== undefined ? value : {}); return value !== undefined ? value : defaults;
} }
FileView { FileView {
@@ -1225,7 +1245,7 @@ Singleton {
id: greeterSessionFile id: greeterSessionFile
path: { path: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"; const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
return greetCfgDir + "/session.json"; return greetCfgDir + "/session.json";
} }
preload: isGreeterMode preload: isGreeterMode
+27 -107
View File
@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
Singleton { Singleton {
id: root id: root
readonly property int settingsConfigVersion: 5 readonly property int settingsConfigVersion: 6
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"
@@ -37,6 +37,18 @@ Singleton {
Custom Custom
} }
enum AnimationVariant {
Material,
Fluent,
Dynamic
}
enum AnimationEffect {
Standard, // 0 M3: scale-in, rises from below
Directional, // 1 pure large slide, no scale
Depth // 2 medium slide with deep depth scale pop
}
enum SuspendBehavior { enum SuspendBehavior {
Suspend, Suspend,
Hibernate, Hibernate,
@@ -130,7 +142,6 @@ Singleton {
property string customThemeFile: "" property string customThemeFile: ""
property var registryThemeVariants: ({}) property var registryThemeVariants: ({})
property string matugenScheme: "scheme-tonal-spot" property string matugenScheme: "scheme-tonal-spot"
property real matugenContrast: 0
property bool runUserMatugenTemplates: true property bool runUserMatugenTemplates: true
property string matugenTargetMonitor: "" property string matugenTargetMonitor: ""
property real popupTransparency: 1.0 property real popupTransparency: 1.0
@@ -151,7 +162,6 @@ Singleton {
property int mangoLayoutBorderSize: -1 property int mangoLayoutBorderSize: -1
property int firstDayOfWeek: -1 property int firstDayOfWeek: -1
property bool showWeekNumber: false
property bool use24HourClock: true property bool use24HourClock: true
property bool showSeconds: false property bool showSeconds: false
property bool padHours12Hour: false property bool padHours12Hour: false
@@ -168,6 +178,12 @@ Singleton {
property int modalCustomAnimationDuration: 150 property int modalCustomAnimationDuration: 150
property bool enableRippleEffects: true property bool enableRippleEffects: true
onEnableRippleEffectsChanged: saveSettings() onEnableRippleEffectsChanged: saveSettings()
property int animationVariant: SettingsData.AnimationVariant.Material
onAnimationVariantChanged: saveSettings()
property int motionEffect: SettingsData.AnimationEffect.Standard
onMotionEffectChanged: saveSettings()
property int directionalAnimationMode: 0
onDirectionalAnimationModeChanged: saveSettings()
property bool m3ElevationEnabled: true property bool m3ElevationEnabled: true
onM3ElevationEnabledChanged: saveSettings() onM3ElevationEnabledChanged: saveSettings()
property int m3ElevationIntensity: 12 property int m3ElevationIntensity: 12
@@ -282,7 +298,6 @@ Singleton {
property bool showOccupiedWorkspacesOnly: false property bool showOccupiedWorkspacesOnly: false
property bool reverseScrolling: false property bool reverseScrolling: false
property bool dwlShowAllTags: false property bool dwlShowAllTags: false
property bool workspaceActiveAppHighlightEnabled: false
property string workspaceColorMode: "default" property string workspaceColorMode: "default"
property string workspaceOccupiedColorMode: "none" property string workspaceOccupiedColorMode: "none"
property string workspaceUnfocusedColorMode: "default" property string workspaceUnfocusedColorMode: "default"
@@ -316,17 +331,6 @@ Singleton {
property string centeringMode: "index" property string centeringMode: "index"
property string clockDateFormat: "" property string clockDateFormat: ""
property string lockDateFormat: "" property string lockDateFormat: ""
property bool greeterRememberLastSession: true
property bool greeterRememberLastUser: true
property bool greeterEnableFprint: false
property bool greeterEnableU2f: false
property string greeterWallpaperPath: ""
property bool greeterUse24HourClock: true
property bool greeterShowSeconds: false
property bool greeterPadHours12Hour: false
property string greeterLockDateFormat: ""
property string greeterFontFamily: ""
property string greeterWallpaperFillMode: ""
property int mediaSize: 1 property int mediaSize: 1
property string appLauncherViewMode: "list" property string appLauncherViewMode: "list"
@@ -455,11 +459,6 @@ Singleton {
property bool syncModeWithPortal: true property bool syncModeWithPortal: true
property bool terminalsAlwaysDark: false property bool terminalsAlwaysDark: false
property string muxType: "tmux"
property bool muxUseCustomCommand: false
property string muxCustomCommand: ""
property string muxSessionFilter: ""
property bool runDmsMatugenTemplates: true property bool runDmsMatugenTemplates: true
property bool matugenTemplateGtk: true property bool matugenTemplateGtk: true
property bool matugenTemplateNiri: true property bool matugenTemplateNiri: true
@@ -475,31 +474,18 @@ Singleton {
property bool matugenTemplateGhostty: true property bool matugenTemplateGhostty: true
property bool matugenTemplateKitty: true property bool matugenTemplateKitty: true
property bool matugenTemplateFoot: true property bool matugenTemplateFoot: true
property bool matugenTemplateNeovim: false property bool matugenTemplateNeovim: true
property bool matugenTemplateAlacritty: true property bool matugenTemplateAlacritty: true
property bool matugenTemplateWezterm: true property bool matugenTemplateWezterm: true
property bool matugenTemplateDgop: true property bool matugenTemplateDgop: true
property bool matugenTemplateKcolorscheme: true property bool matugenTemplateKcolorscheme: true
property bool matugenTemplateVscode: true property bool matugenTemplateVscode: true
property bool matugenTemplateEmacs: true property bool matugenTemplateEmacs: true
property bool matugenTemplateZed: true
property var matugenTemplateNeovimSettings: ({
"dark": {
"baseTheme": "github_dark",
"harmony": 0.5
},
"light": {
"baseTheme": "github_light",
"harmony": 0.5
}
})
property bool showDock: false property bool showDock: false
property bool dockAutoHide: false property bool dockAutoHide: false
property bool dockSmartAutoHide: false property bool dockSmartAutoHide: false
property bool dockGroupByApp: false property bool dockGroupByApp: false
property bool dockRestoreSpecialWorkspaceOnClick: false
property bool dockOpenOnOverview: false property bool dockOpenOnOverview: false
property int dockPosition: SettingsData.Position.Bottom property int dockPosition: SettingsData.Position.Bottom
property real dockSpacing: 4 property real dockSpacing: 4
@@ -545,23 +531,9 @@ Singleton {
property bool enableFprint: false property bool enableFprint: false
property int maxFprintTries: 15 property int maxFprintTries: 15
property bool fprintdAvailable: false property bool fprintdAvailable: false
property bool lockFingerprintCanEnable: false
property bool lockFingerprintReady: false
property string lockFingerprintReason: "probe_failed"
property bool greeterFingerprintCanEnable: false
property bool greeterFingerprintReady: false
property string greeterFingerprintReason: "probe_failed"
property string greeterFingerprintSource: "none"
property bool enableU2f: false property bool enableU2f: false
property string u2fMode: "or" property string u2fMode: "or"
property bool u2fAvailable: false property bool u2fAvailable: false
property bool lockU2fCanEnable: false
property bool lockU2fReady: false
property string lockU2fReason: "probe_failed"
property bool greeterU2fCanEnable: false
property bool greeterU2fReady: false
property string greeterU2fReason: "probe_failed"
property string greeterU2fSource: "none"
property string lockScreenActiveMonitor: "all" property string lockScreenActiveMonitor: "all"
property string lockScreenInactiveColor: "#000000" property string lockScreenInactiveColor: "#000000"
property int lockScreenNotificationMode: 0 property int lockScreenNotificationMode: 0
@@ -584,7 +556,6 @@ Singleton {
property bool notificationHistorySaveNormal: true property bool notificationHistorySaveNormal: true
property bool notificationHistorySaveCritical: true property bool notificationHistorySaveCritical: true
property var notificationRules: [] property var notificationRules: []
property bool notificationFocusedMonitor: false
property bool osdAlwaysShowValue: false property bool osdAlwaysShowValue: false
property int osdPosition: SettingsData.Position.BottomCenter property int osdPosition: SettingsData.Position.BottomCenter
@@ -1048,19 +1019,13 @@ Singleton {
signal widgetDataChanged signal widgetDataChanged
signal workspaceIconsUpdated signal workspaceIconsUpdated
function refreshAuthAvailability() {
if (isGreeterMode)
return;
Processes.settingsRoot = root;
Processes.detectAuthCapabilities();
}
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) { if (!isGreeterMode) {
Processes.settingsRoot = root; Processes.settingsRoot = root;
loadSettings(); loadSettings();
initializeListModels(); initializeListModels();
refreshAuthAvailability(); Processes.detectFprintd();
Processes.detectU2f();
Processes.checkPluginSettings(); Processes.checkPluginSettings();
} }
} }
@@ -1208,7 +1173,7 @@ Singleton {
"updateCompositorLayout": updateCompositorLayout, "updateCompositorLayout": updateCompositorLayout,
"applyStoredIconTheme": applyStoredIconTheme, "applyStoredIconTheme": applyStoredIconTheme,
"updateBarConfigs": updateBarConfigs, "updateBarConfigs": updateBarConfigs,
"updateCompositorCursor": updateCompositorCursor "updateCompositorCursor": updateCompositorCursor,
}) })
function set(key, value) { function set(key, value) {
@@ -1300,45 +1265,10 @@ Singleton {
return JSON.stringify(Store.toJson(root), null, 2); return JSON.stringify(Store.toJson(root), null, 2);
} }
function _resetPluginSettings() {
_pluginParseError = false;
pluginSettings = {};
}
function _pluginSettingsErrorCode(error) {
if (typeof error === "number")
return error;
if (error && typeof error === "object") {
if (typeof error.code === "number")
return error.code;
if (typeof error.errno === "number")
return error.errno;
}
const msg = String(error || "").trim();
if (/^\d+$/.test(msg))
return Number(msg);
return -1;
}
function _isMissingPluginSettingsError(error) {
if (_pluginSettingsErrorCode(error) === 2)
return true;
const msg = String(error || "").toLowerCase();
return msg.indexOf("file does not exist") !== -1 || msg.indexOf("no such file") !== -1 || msg.indexOf("enoent") !== -1;
}
function loadPluginSettings() { function loadPluginSettings() {
try { _pluginSettingsLoading = true;
parsePluginSettings(pluginSettingsFile.text()); parsePluginSettings(pluginSettingsFile.text());
} catch (e) { _pluginSettingsLoading = false;
const msg = e.message || String(e);
if (!_isMissingPluginSettingsError(e))
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
_resetPluginSettings();
}
} }
function parsePluginSettings(content) { function parsePluginSettings(content) {
@@ -1947,12 +1877,6 @@ Singleton {
} }
} }
function setMatugenContrast(value) {
if (matugenContrast === value)
return;
set("matugenContrast", value);
}
function setRunUserMatugenTemplates(enabled) { function setRunUserMatugenTemplates(enabled) {
if (runUserMatugenTemplates === enabled) if (runUserMatugenTemplates === enabled)
return; return;
@@ -2780,7 +2704,6 @@ Singleton {
blockLoading: true blockLoading: true
blockWrites: true blockWrites: true
atomicWrites: true atomicWrites: true
printErrors: false
watchChanges: !isGreeterMode watchChanges: !isGreeterMode
onLoaded: { onLoaded: {
if (!isGreeterMode) { if (!isGreeterMode) {
@@ -2789,10 +2712,7 @@ Singleton {
} }
onLoadFailed: error => { onLoadFailed: error => {
if (!isGreeterMode) { if (!isGreeterMode) {
const msg = String(error || ""); pluginSettings = {};
if (!_isMissingPluginSettingsError(error))
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
_resetPluginSettings();
} }
} }
} }
+26 -18
View File
@@ -960,6 +960,24 @@ Singleton {
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1] "expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
} }
// Delegates to AnimVariants.qml for curves, timing, scale, and offsets.
readonly property list<real> variantEnterCurve: AnimVariants.variantEnterCurve
readonly property list<real> variantExitCurve: AnimVariants.variantExitCurve
readonly property list<real> variantModalEnterCurve: AnimVariants.variantModalEnterCurve
readonly property list<real> variantModalExitCurve: AnimVariants.variantModalExitCurve
readonly property list<real> variantPopoutEnterCurve: AnimVariants.variantPopoutEnterCurve
readonly property list<real> variantPopoutExitCurve: AnimVariants.variantPopoutExitCurve
readonly property real variantEnterDurationFactor: AnimVariants.variantEnterDurationFactor
readonly property real variantExitDurationFactor: AnimVariants.variantExitDurationFactor
readonly property real variantOpacityDurationScale: AnimVariants.variantOpacityDurationScale
readonly property bool isDirectionalEffect: AnimVariants.isDirectionalEffect
readonly property bool isDepthEffect: AnimVariants.isDepthEffect
readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed
readonly property real effectAnimOffset: AnimVariants.effectAnimOffset
function variantDuration(baseDuration, entering) { return AnimVariants.variantDuration(baseDuration, entering); }
function variantExitCleanupPadding() { return AnimVariants.variantExitCleanupPadding(); }
function variantCloseInterval(baseDuration) { return AnimVariants.variantCloseInterval(baseDuration); }
readonly property var animationPresetDurations: { readonly property var animationPresetDurations: {
"none": 0, "none": 0,
"short": 250, "short": 250,
@@ -1084,7 +1102,7 @@ Singleton {
property string fontFamily: { property string fontFamily: {
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") { if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return GreetdSettings.getEffectiveFontFamily(); return GreetdSettings.fontFamily;
} }
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable"; return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
} }
@@ -1248,8 +1266,7 @@ Singleton {
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) { if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
const defaults = themeData.variants.defaults || {}; const defaults = themeData.variants.defaults || {};
const modeDefaults = defaults[colorMode] || defaults.dark || {}; const modeDefaults = defaults[colorMode] || defaults.dark || {};
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode; const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults;
const stored = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.[colorMode] || modeDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults);
var flavorId = stored.flavor || modeDefaults.flavor || ""; var flavorId = stored.flavor || modeDefaults.flavor || "";
const accentId = stored.accent || modeDefaults.accent || ""; const accentId = stored.accent || modeDefaults.accent || "";
var flavor = findVariant(themeData.variants.flavors, flavorId); var flavor = findVariant(themeData.variants.flavors, flavorId);
@@ -1275,8 +1292,7 @@ Singleton {
} }
if (themeData.variants.options && themeData.variants.options.length > 0) { if (themeData.variants.options && themeData.variants.options.length > 0) {
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode; const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default;
const selectedVariantId = isGreeterMode ? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : themeData.variants.default) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default);
const variant = findVariant(themeData.variants.options, selectedVariantId); const variant = findVariant(themeData.variants.options, selectedVariantId);
if (variant) { if (variant) {
const variantColors = variant[colorMode] || variant.dark || variant.light || {}; const variantColors = variant[colorMode] || variant.dark || variant.light || {};
@@ -1549,14 +1565,11 @@ Singleton {
if (typeof SettingsData !== "undefined" && SettingsData.terminalsAlwaysDark) { if (typeof SettingsData !== "undefined" && SettingsData.terminalsAlwaysDark) {
args.push("--terminals-always-dark"); args.push("--terminals-always-dark");
} }
if (typeof SettingsData !== "undefined" && SettingsData.matugenContrast !== 0) {
args.push("--contrast", SettingsData.matugenContrast.toString());
}
if (typeof SettingsData !== "undefined") { if (typeof SettingsData !== "undefined") {
const skipTemplates = []; const skipTemplates = [];
if (!SettingsData.runDmsMatugenTemplates) { if (!SettingsData.runDmsMatugenTemplates) {
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs", "zed"); skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs");
} else { } else {
if (!SettingsData.matugenTemplateGtk) if (!SettingsData.matugenTemplateGtk)
skipTemplates.push("gtk"); skipTemplates.push("gtk");
@@ -1600,8 +1613,6 @@ Singleton {
skipTemplates.push("vscode"); skipTemplates.push("vscode");
if (!SettingsData.matugenTemplateEmacs) if (!SettingsData.matugenTemplateEmacs)
skipTemplates.push("emacs"); skipTemplates.push("emacs");
if (!SettingsData.matugenTemplateZed)
skipTemplates.push("zed");
} }
if (skipTemplates.length > 0) { if (skipTemplates.length > 0) {
args.push("--skip-templates", skipTemplates.join(",")); args.push("--skip-templates", skipTemplates.join(","));
@@ -1651,9 +1662,8 @@ Singleton {
const defaults = customThemeRawData.variants.defaults || {}; const defaults = customThemeRawData.variants.defaults || {};
const darkDefaults = defaults.dark || {}; const darkDefaults = defaults.dark || {};
const lightDefaults = defaults.light || defaults.dark || {}; const lightDefaults = defaults.light || defaults.dark || {};
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode; const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults;
const storedDark = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.dark || darkDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults); const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults;
const storedLight = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.light || lightDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults);
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || ""; const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || ""; const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
const accentId = storedDark.accent || darkDefaults.accent || ""; const accentId = storedDark.accent || darkDefaults.accent || "";
@@ -1671,8 +1681,7 @@ Singleton {
lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {}); lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {});
} }
} else if (customThemeRawData.variants.options) { } else if (customThemeRawData.variants.options) {
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode; const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default;
const selectedVariantId = isGreeterMode ? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : customThemeRawData.variants.default) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default);
const variant = findVariant(customThemeRawData.variants.options, selectedVariantId); const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
if (variant) { if (variant) {
darkTheme = mergeColors(darkTheme, variant.dark || {}); darkTheme = mergeColors(darkTheme, variant.dark || {});
@@ -1996,11 +2005,10 @@ Singleton {
FileView { FileView {
id: dynamicColorsFileView id: dynamicColorsFileView
path: { path: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"; const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json"; const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
return colorsPath; return colorsPath;
} }
blockLoading: false
watchChanges: !SessionData.isGreeterMode watchChanges: !SessionData.isGreeterMode
function parseAndLoadColors() { function parseAndLoadColors() {
+21 -509
View File
@@ -10,352 +10,22 @@ Singleton {
property var settingsRoot: null property var settingsRoot: null
property string greetdPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
property string passwordAuthPamText: ""
property string systemLoginPamText: ""
property string systemLocalLoginPamText: ""
property string commonAuthPcPamText: ""
property string loginPamText: ""
property string dankshellU2fPamText: ""
property string u2fKeysText: ""
property string fingerprintProbeOutput: ""
property int fingerprintProbeExitCode: 0
property bool fingerprintProbeStreamFinished: false
property bool fingerprintProbeExited: false
property string fingerprintProbeState: "probe_failed"
property string pamSupportProbeOutput: ""
property bool pamSupportProbeStreamFinished: false
property bool pamSupportProbeExited: false
property int pamSupportProbeExitCode: 0
property bool pamFprintSupportDetected: false
property bool pamU2fSupportDetected: false
readonly property string homeDir: Quickshell.env("HOME") || ""
readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : ""
readonly property bool homeU2fKeysDetected: u2fKeysPath !== "" && u2fKeysWatcher.loaded && u2fKeysText.trim() !== ""
readonly property bool lockU2fCustomConfigDetected: pamModuleEnabled(dankshellU2fPamText, "pam_u2f")
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd")
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
function envFlag(name) {
const value = (Quickshell.env(name) || "").trim().toLowerCase();
if (value === "1" || value === "true" || value === "yes" || value === "on")
return true;
if (value === "0" || value === "false" || value === "no" || value === "off")
return false;
return null;
}
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE")
function detectQtTools() { function detectQtTools() {
qtToolsDetectionProcess.running = true; qtToolsDetectionProcess.running = true;
} }
function detectAuthCapabilities() {
if (!settingsRoot)
return;
if (forcedFprintAvailable === null) {
fingerprintProbeOutput = "";
fingerprintProbeStreamFinished = false;
fingerprintProbeExited = false;
fingerprintProbeProcess.running = true;
} else {
fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed";
}
if (forcedFprintAvailable === null || forcedU2fAvailable === null) {
pamFprintSupportDetected = false;
pamU2fSupportDetected = false;
pamSupportProbeOutput = "";
pamSupportProbeStreamFinished = false;
pamSupportProbeExited = false;
pamSupportDetectionProcess.running = true;
}
recomputeAuthCapabilities();
}
function detectFprintd() { function detectFprintd() {
detectAuthCapabilities(); fprintdDetectionProcess.running = true;
} }
function detectU2f() { function detectU2f() {
detectAuthCapabilities(); u2fDetectionProcess.running = true;
} }
function checkPluginSettings() { function checkPluginSettings() {
pluginSettingsCheckProcess.running = true; pluginSettingsCheckProcess.running = true;
} }
function stripPamComment(line) {
if (!line)
return "";
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#"))
return "";
const hashIdx = trimmed.indexOf("#");
if (hashIdx >= 0)
return trimmed.substring(0, hashIdx).trim();
return trimmed;
}
function pamModuleEnabled(pamText, moduleName) {
if (!pamText || !moduleName)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes(moduleName))
return true;
}
return false;
}
function pamTextIncludesFile(pamText, filename) {
if (!pamText || !filename)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include")))
return true;
}
return false;
}
function greeterPamStackHasModule(moduleName) {
if (pamModuleEnabled(greetdPamText, moduleName))
return true;
const includedPamStacks = [
["system-auth", systemAuthPamText],
["common-auth", commonAuthPamText],
["password-auth", passwordAuthPamText],
["system-login", systemLoginPamText],
["system-local-login", systemLocalLoginPamText],
["common-auth-pc", commonAuthPcPamText],
["login", loginPamText]
];
for (let i = 0; i < includedPamStacks.length; i++) {
const stack = includedPamStacks[i];
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
return true;
}
return false;
}
function hasEnrolledFingerprintOutput(output) {
const lower = (output || "").toLowerCase();
if (lower.includes("has fingers enrolled") || lower.includes("has fingerprints enrolled"))
return true;
const lines = lower.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const trimmed = lines[i].trim();
if (trimmed.startsWith("finger:"))
return true;
if (trimmed.startsWith("- ") && trimmed.includes("finger"))
return true;
}
return false;
}
function hasMissingFingerprintEnrollmentOutput(output) {
const lower = (output || "").toLowerCase();
return lower.includes("no fingers enrolled")
|| lower.includes("no fingerprints enrolled")
|| lower.includes("no prints enrolled");
}
function hasMissingFingerprintReaderOutput(output) {
const lower = (output || "").toLowerCase();
return lower.includes("no devices available")
|| lower.includes("no device available")
|| lower.includes("no devices found")
|| lower.includes("list_devices failed")
|| lower.includes("no device");
}
function parseFingerprintProbe(exitCode, output) {
if (hasEnrolledFingerprintOutput(output))
return "ready";
if (hasMissingFingerprintEnrollmentOutput(output))
return "missing_enrollment";
if (hasMissingFingerprintReaderOutput(output))
return "missing_reader";
if (exitCode === 0)
return "missing_enrollment";
if (exitCode === 127 || (output || "").includes("__missing_command__"))
return "probe_failed";
return pamFprintSupportDetected ? "probe_failed" : "missing_pam_support";
}
function setLockFingerprintCapability(canEnable, ready, reason) {
settingsRoot.lockFingerprintCanEnable = canEnable;
settingsRoot.lockFingerprintReady = ready;
settingsRoot.lockFingerprintReason = reason;
}
function setLockU2fCapability(canEnable, ready, reason) {
settingsRoot.lockU2fCanEnable = canEnable;
settingsRoot.lockU2fReady = ready;
settingsRoot.lockU2fReason = reason;
}
function setGreeterFingerprintCapability(canEnable, ready, reason, source) {
settingsRoot.greeterFingerprintCanEnable = canEnable;
settingsRoot.greeterFingerprintReady = ready;
settingsRoot.greeterFingerprintReason = reason;
settingsRoot.greeterFingerprintSource = source;
}
function setGreeterU2fCapability(canEnable, ready, reason, source) {
settingsRoot.greeterU2fCanEnable = canEnable;
settingsRoot.greeterU2fReady = ready;
settingsRoot.greeterU2fReason = reason;
settingsRoot.greeterU2fSource = source;
}
function recomputeFingerprintCapabilities() {
if (forcedFprintAvailable !== null) {
const reason = forcedFprintAvailable ? "ready" : "probe_failed";
const source = forcedFprintAvailable ? "dms" : "none";
setLockFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason);
setGreeterFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason, source);
return;
}
const state = fingerprintProbeState;
switch (state) {
case "ready":
setLockFingerprintCapability(true, true, "ready");
break;
case "missing_enrollment":
setLockFingerprintCapability(true, false, "missing_enrollment");
break;
case "missing_reader":
setLockFingerprintCapability(false, false, "missing_reader");
break;
case "missing_pam_support":
setLockFingerprintCapability(false, false, "missing_pam_support");
break;
default:
setLockFingerprintCapability(false, false, "probe_failed");
break;
}
if (greeterPamHasFprint) {
switch (state) {
case "ready":
setGreeterFingerprintCapability(true, true, "configured_externally", "pam");
break;
case "missing_enrollment":
setGreeterFingerprintCapability(true, false, "missing_enrollment", "pam");
break;
case "missing_reader":
setGreeterFingerprintCapability(false, false, "missing_reader", "pam");
break;
default:
setGreeterFingerprintCapability(true, false, "probe_failed", "pam");
break;
}
return;
}
switch (state) {
case "ready":
setGreeterFingerprintCapability(true, true, "ready", "dms");
break;
case "missing_enrollment":
setGreeterFingerprintCapability(true, false, "missing_enrollment", "dms");
break;
case "missing_reader":
setGreeterFingerprintCapability(false, false, "missing_reader", "none");
break;
case "missing_pam_support":
setGreeterFingerprintCapability(false, false, "missing_pam_support", "none");
break;
default:
setGreeterFingerprintCapability(false, false, "probe_failed", "none");
break;
}
}
function recomputeU2fCapabilities() {
if (forcedU2fAvailable !== null) {
const reason = forcedU2fAvailable ? "ready" : "probe_failed";
const source = forcedU2fAvailable ? "dms" : "none";
setLockU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason);
setGreeterU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason, source);
return;
}
const lockReady = lockU2fCustomConfigDetected || homeU2fKeysDetected;
const lockCanEnable = lockReady || pamU2fSupportDetected;
const lockReason = lockReady ? "ready" : (lockCanEnable ? "missing_key_registration" : "missing_pam_support");
setLockU2fCapability(lockCanEnable, lockReady, lockReason);
if (greeterPamHasU2f) {
setGreeterU2fCapability(true, true, "configured_externally", "pam");
return;
}
const greeterReady = homeU2fKeysDetected;
const greeterCanEnable = greeterReady || pamU2fSupportDetected;
const greeterReason = greeterReady ? "ready" : (greeterCanEnable ? "missing_key_registration" : "missing_pam_support");
setGreeterU2fCapability(greeterCanEnable, greeterReady, greeterReason, greeterCanEnable ? "dms" : "none");
}
function recomputeAuthCapabilities() {
if (!settingsRoot)
return;
recomputeFingerprintCapabilities();
recomputeU2fCapabilities();
settingsRoot.fprintdAvailable = settingsRoot.lockFingerprintReady || settingsRoot.greeterFingerprintReady;
settingsRoot.u2fAvailable = settingsRoot.lockU2fReady || settingsRoot.greeterU2fReady;
}
function finalizeFingerprintProbe() {
if (!fingerprintProbeStreamFinished || !fingerprintProbeExited)
return;
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
recomputeAuthCapabilities();
}
function finalizePamSupportProbe() {
if (!pamSupportProbeStreamFinished || !pamSupportProbeExited)
return;
pamFprintSupportDetected = false;
pamU2fSupportDetected = false;
const lines = (pamSupportProbeOutput || "").trim().split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const parts = lines[i].split(":");
if (parts.length !== 2)
continue;
if (parts[0] === "pam_fprintd.so")
pamFprintSupportDetected = parts[1] === "true";
else if (parts[0] === "pam_u2f.so")
pamU2fSupportDetected = parts[1] === "true";
}
if (forcedFprintAvailable === null && fingerprintProbeState === "missing_pam_support")
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
recomputeAuthCapabilities();
}
property var qtToolsDetectionProcess: Process { property var qtToolsDetectionProcess: Process {
command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"] command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"]
running: false running: false
@@ -365,15 +35,15 @@ Singleton {
if (!settingsRoot) if (!settingsRoot)
return; return;
if (text && text.trim()) { if (text && text.trim()) {
const lines = text.trim().split("\n"); var lines = text.trim().split('\n');
for (let i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
const line = lines[i]; var line = lines[i];
if (line.startsWith("qt5ct:")) { if (line.startsWith('qt5ct:')) {
settingsRoot.qt5ctAvailable = line.split(":")[1] === "true"; settingsRoot.qt5ctAvailable = line.split(':')[1] === 'true';
} else if (line.startsWith("qt6ct:")) { } else if (line.startsWith('qt6ct:')) {
settingsRoot.qt6ctAvailable = line.split(":")[1] === "true"; settingsRoot.qt6ctAvailable = line.split(':')[1] === 'true';
} else if (line.startsWith("gtk:")) { } else if (line.startsWith('gtk:')) {
settingsRoot.gtkAvailable = line.split(":")[1] === "true"; settingsRoot.gtkAvailable = line.split(':')[1] === 'true';
} }
} }
} }
@@ -381,181 +51,23 @@ Singleton {
} }
} }
property var fingerprintProbeProcess: Process { property var fprintdDetectionProcess: Process {
command: ["sh", "-c", "if command -v fprintd-list >/dev/null 2>&1; then fprintd-list \"${USER:-$(id -un)}\" 2>&1; else printf '__missing_command__\\n'; exit 127; fi"] command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"]
running: false running: false
stdout: StdioCollector {
onStreamFinished: {
root.fingerprintProbeOutput = text || "";
root.fingerprintProbeStreamFinished = true;
root.finalizeFingerprintProbe();
}
}
onExited: function (exitCode) { onExited: function (exitCode) {
root.fingerprintProbeExitCode = exitCode; if (!settingsRoot)
root.fingerprintProbeExited = true; return;
root.finalizeFingerprintProbe(); settingsRoot.fprintdAvailable = (exitCode === 0);
} }
} }
property var pamSupportDetectionProcess: Process { property var u2fDetectionProcess: Process {
command: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"] command: ["sh", "-c", "(test -f /usr/lib/security/pam_u2f.so || test -f /usr/lib64/security/pam_u2f.so) && (test -f /etc/pam.d/dankshell-u2f || test -f \"$HOME/.config/Yubico/u2f_keys\")"]
running: false running: false
stdout: StdioCollector {
onStreamFinished: {
root.pamSupportProbeOutput = text || "";
root.pamSupportProbeStreamFinished = true;
root.finalizePamSupportProbe();
}
}
onExited: function (exitCode) { onExited: function (exitCode) {
root.pamSupportProbeExitCode = exitCode; if (!settingsRoot)
root.pamSupportProbeExited = true; return;
root.finalizePamSupportProbe(); settingsRoot.u2fAvailable = (exitCode === 0);
}
}
FileView {
id: greetdPamWatcher
path: "/etc/pam.d/greetd"
printErrors: false
onLoaded: {
root.greetdPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.greetdPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: systemAuthPamWatcher
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: {
root.systemAuthPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.systemAuthPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: commonAuthPamWatcher
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: {
root.commonAuthPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.commonAuthPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: passwordAuthPamWatcher
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: {
root.passwordAuthPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.passwordAuthPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: systemLoginPamWatcher
path: "/etc/pam.d/system-login"
printErrors: false
onLoaded: {
root.systemLoginPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.systemLoginPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: systemLocalLoginPamWatcher
path: "/etc/pam.d/system-local-login"
printErrors: false
onLoaded: {
root.systemLocalLoginPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.systemLocalLoginPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: commonAuthPcPamWatcher
path: "/etc/pam.d/common-auth-pc"
printErrors: false
onLoaded: {
root.commonAuthPcPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.commonAuthPcPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: loginPamWatcher
path: "/etc/pam.d/login"
printErrors: false
onLoaded: {
root.loginPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.loginPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: dankshellU2fPamWatcher
path: "/etc/pam.d/dankshell-u2f"
printErrors: false
onLoaded: {
root.dankshellU2fPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.dankshellU2fPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: u2fKeysWatcher
path: root.u2fKeysPath
printErrors: false
onLoaded: {
root.u2fKeysText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.u2fKeysText = "";
root.recomputeAuthCapabilities();
} }
} }
+1 -4
View File
@@ -84,10 +84,7 @@ var SPEC = {
launcherLastMode: { def: "all" }, launcherLastMode: { def: "all" },
appDrawerLastMode: { def: "apps" }, appDrawerLastMode: { def: "apps" },
niriOverviewLastMode: { def: "apps" }, niriOverviewLastMode: { def: "apps" }
settingsSidebarExpandedIds: { def: "," },
settingsSidebarCollapsedIds: { def: "," }
}; };
function getValidKeys() { function getValidKeys() {
+4 -44
View File
@@ -11,7 +11,6 @@ var SPEC = {
customThemeFile: { def: "" }, customThemeFile: { def: "" },
registryThemeVariants: { def: {} }, registryThemeVariants: { def: {} },
matugenScheme: { def: "scheme-tonal-spot", onChange: "regenSystemThemes" }, matugenScheme: { def: "scheme-tonal-spot", onChange: "regenSystemThemes" },
matugenContrast: { def: 0, onChange: "regenSystemThemes" },
runUserMatugenTemplates: { def: true, onChange: "regenSystemThemes" }, runUserMatugenTemplates: { def: true, onChange: "regenSystemThemes" },
matugenTargetMonitor: { def: "", onChange: "regenSystemThemes" }, matugenTargetMonitor: { def: "", onChange: "regenSystemThemes" },
@@ -34,7 +33,6 @@ var SPEC = {
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" }, mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
firstDayOfWeek: { def: -1 }, firstDayOfWeek: { def: -1 },
showWeekNumber: { def: false },
use24HourClock: { def: true }, use24HourClock: { def: true },
showSeconds: { def: false }, showSeconds: { def: false },
padHours12Hour: { def: false }, padHours12Hour: { def: false },
@@ -49,6 +47,9 @@ var SPEC = {
modalAnimationSpeed: { def: 1 }, modalAnimationSpeed: { def: 1 },
modalCustomAnimationDuration: { def: 150 }, modalCustomAnimationDuration: { def: 150 },
enableRippleEffects: { def: true }, enableRippleEffects: { def: true },
animationVariant: { def: 0 },
motionEffect: { def: 0 },
directionalAnimationMode: { def: 0 },
m3ElevationEnabled: { def: true }, m3ElevationEnabled: { def: true },
m3ElevationIntensity: { def: 12 }, m3ElevationIntensity: { def: 12 },
m3ElevationOpacity: { def: 30 }, m3ElevationOpacity: { def: 30 },
@@ -125,7 +126,6 @@ var SPEC = {
showOccupiedWorkspacesOnly: { def: false }, showOccupiedWorkspacesOnly: { def: false },
reverseScrolling: { def: false }, reverseScrolling: { def: false },
dwlShowAllTags: { def: false }, dwlShowAllTags: { def: false },
workspaceActiveAppHighlightEnabled: { def: false },
workspaceColorMode: { def: "default" }, workspaceColorMode: { def: "default" },
workspaceOccupiedColorMode: { def: "none" }, workspaceOccupiedColorMode: { def: "none" },
workspaceUnfocusedColorMode: { def: "default" }, workspaceUnfocusedColorMode: { def: "default" },
@@ -167,17 +167,6 @@ var SPEC = {
centeringMode: { def: "index" }, centeringMode: { def: "index" },
clockDateFormat: { def: "" }, clockDateFormat: { def: "" },
lockDateFormat: { def: "" }, lockDateFormat: { def: "" },
greeterRememberLastSession: { def: true },
greeterRememberLastUser: { def: true },
greeterEnableFprint: { def: false },
greeterEnableU2f: { def: false },
greeterWallpaperPath: { def: "" },
greeterUse24HourClock: { def: true },
greeterShowSeconds: { def: false },
greeterPadHours12Hour: { def: false },
greeterLockDateFormat: { def: "" },
greeterFontFamily: { def: "" },
greeterWallpaperFillMode: { def: "" },
mediaSize: { def: 1 }, mediaSize: { def: 1 },
appLauncherViewMode: { def: "list" }, appLauncherViewMode: { def: "list" },
@@ -270,11 +259,6 @@ var SPEC = {
syncModeWithPortal: { def: true }, syncModeWithPortal: { def: true },
terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" }, terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" },
muxType: { def: "tmux" },
muxUseCustomCommand: { def: false },
muxCustomCommand: { def: "" },
muxSessionFilter: { def: "" },
runDmsMatugenTemplates: { def: true }, runDmsMatugenTemplates: { def: true },
matugenTemplateGtk: { def: true }, matugenTemplateGtk: { def: true },
matugenTemplateNiri: { def: true }, matugenTemplateNiri: { def: true },
@@ -291,26 +275,17 @@ var SPEC = {
matugenTemplateKitty: { def: true }, matugenTemplateKitty: { def: true },
matugenTemplateFoot: { def: true }, matugenTemplateFoot: { def: true },
matugenTemplateAlacritty: { def: true }, matugenTemplateAlacritty: { def: true },
matugenTemplateNeovim: { def: false }, matugenTemplateNeovim: { def: true },
matugenTemplateWezterm: { def: true }, matugenTemplateWezterm: { def: true },
matugenTemplateDgop: { def: true }, matugenTemplateDgop: { def: true },
matugenTemplateKcolorscheme: { def: true }, matugenTemplateKcolorscheme: { def: true },
matugenTemplateVscode: { def: true }, matugenTemplateVscode: { def: true },
matugenTemplateEmacs: { def: true }, matugenTemplateEmacs: { def: true },
matugenTemplateZed: { def: true },
matugenTemplateNeovimSettings: {
def: {
dark: { baseTheme: "github_dark", harmony: 0.5 },
light: { baseTheme: "github_light", harmony: 0.5 }
}
},
showDock: { def: false }, showDock: { def: false },
dockAutoHide: { def: false }, dockAutoHide: { def: false },
dockSmartAutoHide: { def: false }, dockSmartAutoHide: { def: false },
dockGroupByApp: { def: false }, dockGroupByApp: { def: false },
dockRestoreSpecialWorkspaceOnClick: { def: false },
dockOpenOnOverview: { def: false }, dockOpenOnOverview: { def: false },
dockPosition: { def: 1 }, dockPosition: { def: 1 },
dockSpacing: { def: 4 }, dockSpacing: { def: 4 },
@@ -355,23 +330,9 @@ var SPEC = {
enableFprint: { def: false }, enableFprint: { def: false },
maxFprintTries: { def: 15 }, maxFprintTries: { def: 15 },
fprintdAvailable: { def: false, persist: false }, fprintdAvailable: { def: false, persist: false },
lockFingerprintCanEnable: { def: false, persist: false },
lockFingerprintReady: { def: false, persist: false },
lockFingerprintReason: { def: "probe_failed", persist: false },
greeterFingerprintCanEnable: { def: false, persist: false },
greeterFingerprintReady: { def: false, persist: false },
greeterFingerprintReason: { def: "probe_failed", persist: false },
greeterFingerprintSource: { def: "none", persist: false },
enableU2f: { def: false }, enableU2f: { def: false },
u2fMode: { def: "or" }, u2fMode: { def: "or" },
u2fAvailable: { def: false, persist: false }, u2fAvailable: { def: false, persist: false },
lockU2fCanEnable: { def: false, persist: false },
lockU2fReady: { def: false, persist: false },
lockU2fReason: { def: "probe_failed", persist: false },
greeterU2fCanEnable: { def: false, persist: false },
greeterU2fReady: { def: false, persist: false },
greeterU2fReason: { def: "probe_failed", persist: false },
greeterU2fSource: { def: "none", persist: false },
lockScreenActiveMonitor: { def: "all" }, lockScreenActiveMonitor: { def: "all" },
lockScreenInactiveColor: { def: "#000000" }, lockScreenInactiveColor: { def: "#000000" },
lockScreenNotificationMode: { def: 0 }, lockScreenNotificationMode: { def: 0 },
@@ -394,7 +355,6 @@ var SPEC = {
notificationHistorySaveNormal: { def: true }, notificationHistorySaveNormal: { def: true },
notificationHistorySaveCritical: { def: true }, notificationHistorySaveCritical: { def: true },
notificationRules: { def: [] }, notificationRules: { def: [] },
notificationFocusedMonitor: { def: false },
osdAlwaysShowValue: { def: false }, osdAlwaysShowValue: { def: false },
osdPosition: { def: 5 }, osdPosition: { def: 5 },
+2
View File
@@ -1,5 +1,7 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Services.Greetd
import qs.Common
import qs.Modules.Greetd import qs.Modules.Greetd
Scope { Scope {
+4 -7
View File
@@ -313,7 +313,7 @@ Item {
} }
Variants { Variants {
model: SettingsData.notificationFocusedMonitor ? Quickshell.screens : SettingsData.getFilteredScreens("notifications") model: SettingsData.getFilteredScreens("notifications")
delegate: NotificationPopupManager { delegate: NotificationPopupManager {
modelData: item modelData: item
@@ -619,10 +619,6 @@ Item {
} }
} }
MuxModal {
id: muxModal
}
ClipboardHistoryModal { ClipboardHistoryModal {
id: clipboardHistoryModalPopup id: clipboardHistoryModalPopup
@@ -819,8 +815,9 @@ Item {
content: Component { content: Component {
Notepad { Notepad {
slideout: notepadSlideout onHideRequested: {
onHideRequested: notepadSlideout.hide() notepadSlideout.hide();
}
} }
} }
@@ -26,9 +26,7 @@ Rectangle {
spacing: 2 spacing: 2
StyledText { StyledText {
text: keyboardHints.enterToPaste text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled") : "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled")
: I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
+169 -50
View File
@@ -26,10 +26,10 @@ Item {
property bool closeOnBackgroundClick: true property bool closeOnBackgroundClick: true
property string animationType: "scale" property string animationType: "scale"
property int animationDuration: Theme.modalAnimationDuration property int animationDuration: Theme.modalAnimationDuration
property real animationScaleCollapsed: 0.96 property real animationScaleCollapsed: Theme.effectScaleCollapsed
property real animationOffset: Theme.spacingL property real animationOffset: Theme.effectAnimOffset
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial property list<real> animationEnterCurve: Theme.variantModalEnterCurve
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized property list<real> animationExitCurve: Theme.variantModalExitCurve
property color backgroundColor: Theme.surfaceContainer property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium property color borderColor: Theme.outlineMedium
property real borderWidth: 0 property real borderWidth: 0
@@ -44,11 +44,13 @@ Item {
property bool keepPopoutsOpen: false property bool keepPopoutsOpen: false
property var customKeyboardFocus: null property var customKeyboardFocus: null
property bool useOverlayLayer: false property bool useOverlayLayer: false
property real frozenMotionOffsetX: 0
property real frozenMotionOffsetY: 0
readonly property alias contentWindow: contentWindow readonly property alias contentWindow: contentWindow
readonly property alias clickCatcher: clickCatcher readonly property alias clickCatcher: clickCatcher
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground readonly property bool useSingleWindow: CompositorService.isHyprland
signal opened signal opened
signal dialogClosed signal dialogClosed
@@ -58,19 +60,34 @@ Item {
function open() { function open() {
closeTimer.stop(); closeTimer.stop();
animationsEnabled = false;
frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0;
frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset;
const focusedScreen = CompositorService.getFocusedScreen(); const focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) { if (focusedScreen) {
contentWindow.screen = focusedScreen; contentWindow.screen = focusedScreen;
if (!useSingleWindow) if (!useSingleWindow)
clickCatcher.screen = focusedScreen; clickCatcher.screen = focusedScreen;
} }
if (Theme.isDirectionalEffect || root.useBackground) {
if (!useSingleWindow)
clickCatcher.visible = true;
contentWindow.visible = true;
}
ModalManager.openModal(root); ModalManager.openModal(root);
shouldBeVisible = true;
if (!useSingleWindow) Qt.callLater(() => {
clickCatcher.visible = true; animationsEnabled = true;
contentWindow.visible = true; shouldBeVisible = true;
shouldHaveFocus = false; if (!useSingleWindow && !clickCatcher.visible)
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible)); clickCatcher.visible = true;
if (!contentWindow.visible)
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
});
} }
function close() { function close() {
@@ -131,7 +148,7 @@ Item {
Timer { Timer {
id: closeTimer id: closeTimer
interval: animationDuration + 50 interval: Theme.variantCloseInterval(animationDuration)
onTriggered: { onTriggered: {
if (shouldBeVisible) if (shouldBeVisible)
return; return;
@@ -145,7 +162,17 @@ Item {
readonly property var shadowLevel: Theme.elevationLevel3 readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6 readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0 readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowMotionPadding: animationType === "slide" ? 30 : Math.max(0, animationOffset) readonly property real shadowMotionPadding: {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect)
return 0; // Wayland native overlap mask
if (animationType === "slide")
return 30;
if (Theme.isDirectionalEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.9);
if (Theme.isDepthEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.35);
return Math.max(0, animationOffset);
}
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr) readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr) readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr) readonly property real alignedHeight: Theme.px(modalHeight, dpr)
@@ -205,9 +232,26 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: root.closeOnBackgroundClick && root.shouldBeVisible enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: root.backgroundClicked() onClicked: root.backgroundClicked()
} }
Rectangle {
anchors.fill: parent
z: -1
color: "black"
opacity: (!root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: opacity > 0
Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
NumberAnimation {
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
} }
PanelWindow { PanelWindow {
@@ -250,9 +294,12 @@ Item {
bottom: root.useSingleWindow bottom: root.useSingleWindow
} }
readonly property real actualMarginLeft: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
readonly property real actualMarginTop: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
WlrLayershell.margins { WlrLayershell.margins {
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr)) left: actualMarginLeft
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr)) top: actualMarginTop
right: 0 right: 0
bottom: 0 bottom: 0
} }
@@ -282,13 +329,14 @@ Item {
anchors.fill: parent anchors.fill: parent
z: -1 z: -1
color: "black" color: "black"
opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0 opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.useBackground visible: opacity > 0
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled enabled: root.animationsEnabled && !Theme.isDirectionalEffect
DankAnim { NumberAnimation {
duration: root.animationDuration duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
@@ -296,8 +344,8 @@ Item {
Item { Item {
id: modalContainer id: modalContainer
x: root.useSingleWindow ? root.alignedX : shadowBuffer x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr)
y: root.useSingleWindow ? root.alignedY : shadowBuffer y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr)
width: root.alignedWidth width: root.alignedWidth
height: root.alignedHeight height: root.alignedHeight
@@ -313,45 +361,117 @@ Item {
} }
readonly property bool slide: root.animationType === "slide" readonly property bool slide: root.animationType === "slide"
readonly property real offsetX: slide ? 15 : 0 readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property real offsetY: slide ? -30 : root.animationOffset readonly property bool depthEffect: Theme.isDepthEffect
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
property real animX: 0 readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
property real animY: 0 readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
property real scaleValue: root.animationScaleCollapsed readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
readonly property real customDistLeft: customAnchorX
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr) readonly property real customDistRight: root.screenWidth - customAnchorX
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr) readonly property real customDistTop: customAnchorY
readonly property real customDistBottom: root.screenHeight - customAnchorY
Connections { readonly property real offsetX: {
target: root if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
function onShouldBeVisibleChanged() { return 0;
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr); if (slide && !directionalEffect && !depthEffect)
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr); return 15;
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed; if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -directionalTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return directionalTravel;
return 0;
default:
return 0;
}
} }
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -depthTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return depthTravel;
return 0;
default:
return 0;
}
}
return 0;
} }
readonly property real offsetY: {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
return 0;
if (slide && !directionalEffect && !depthEffect)
return -30;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return -Math.max(directionalTravel * 0.65, 96);
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -directionalTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return directionalTravel;
return 0;
default:
// Default to sliding down from top when centered
return -Math.max(directionalTravel, root.screenHeight * 0.24);
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return -depthTravel * 0.75;
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -depthTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return depthTravel;
return depthTravel * 0.45;
default:
return -depthTravel;
}
}
return root.animationOffset;
}
property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX
property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed
property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed
Behavior on animX { Behavior on animX {
enabled: root.animationsEnabled enabled: root.animationsEnabled
DankAnim { NumberAnimation {
duration: root.animationDuration duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on animY { Behavior on animY {
enabled: root.animationsEnabled enabled: root.animationsEnabled
DankAnim { NumberAnimation {
duration: root.animationDuration duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on scaleValue { Behavior on scaleValue {
enabled: root.animationsEnabled enabled: root.animationsEnabled
DankAnim { NumberAnimation {
duration: root.animationDuration duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
@@ -367,15 +487,14 @@ Item {
id: animatedContent id: animatedContent
anchors.fill: parent anchors.fill: parent
clip: false clip: false
opacity: root.shouldBeVisible ? 1 : 0 opacity: Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0)
scale: modalContainer.scaleValue scale: modalContainer.scaleValue
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5 transformOrigin: Item.Center
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled enabled: root.animationsEnabled && !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: animationDuration duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
-312
View File
@@ -1,312 +0,0 @@
import QtQuick
import qs.Common
import qs.Modals.Common
import qs.Widgets
DankModal {
id: root
layerNamespace: "dms:input-modal"
keepPopoutsOpen: true
property string inputTitle: ""
property string inputMessage: ""
property string inputPlaceholder: ""
property string inputText: ""
property string confirmButtonText: "Confirm"
property string cancelButtonText: "Cancel"
property color confirmButtonColor: Theme.primary
property var onConfirm: function (text) {}
property var onCancel: function () {}
property int selectedButton: -1
property bool keyboardNavigation: false
function show(title, message, onConfirmCallback, onCancelCallback) {
inputTitle = title || "";
inputMessage = message || "";
inputPlaceholder = "";
inputText = "";
confirmButtonText = "Confirm";
cancelButtonText = "Cancel";
confirmButtonColor = Theme.primary;
onConfirm = onConfirmCallback || ((text) => {});
onCancel = onCancelCallback || (() => {});
selectedButton = -1;
keyboardNavigation = false;
open();
}
function showWithOptions(options) {
inputTitle = options.title || "";
inputMessage = options.message || "";
inputPlaceholder = options.placeholder || "";
inputText = options.initialText || "";
confirmButtonText = options.confirmText || "Confirm";
cancelButtonText = options.cancelText || "Cancel";
confirmButtonColor = options.confirmColor || Theme.primary;
onConfirm = options.onConfirm || ((text) => {});
onCancel = options.onCancel || (() => {});
selectedButton = -1;
keyboardNavigation = false;
open();
}
function confirmAndClose() {
const text = inputText;
close();
if (onConfirm) {
onConfirm(text);
}
}
function cancelAndClose() {
close();
if (onCancel) {
onCancel();
}
}
function selectButton() {
if (selectedButton === 0) {
cancelAndClose();
} else {
confirmAndClose();
}
}
shouldBeVisible: false
allowStacking: true
modalWidth: 350
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200
enableShadow: true
shouldHaveFocus: true
onBackgroundClicked: cancelAndClose()
onOpened: {
Qt.callLater(function () {
if (contentLoader.item && contentLoader.item.textInputRef) {
contentLoader.item.textInputRef.forceActiveFocus();
}
});
}
content: Component {
FocusScope {
anchors.fill: parent
implicitHeight: mainColumn.implicitHeight
focus: true
property alias textInputRef: textInput
Keys.onPressed: function (event) {
const textFieldFocused = textInput.activeFocus;
switch (event.key) {
case Qt.Key_Escape:
root.cancelAndClose();
event.accepted = true;
break;
case Qt.Key_Tab:
if (textFieldFocused) {
root.keyboardNavigation = true;
root.selectedButton = 0;
textInput.focus = false;
} else {
root.keyboardNavigation = true;
if (root.selectedButton === -1) {
root.selectedButton = 0;
} else if (root.selectedButton === 0) {
root.selectedButton = 1;
} else {
root.selectedButton = -1;
textInput.forceActiveFocus();
}
}
event.accepted = true;
break;
case Qt.Key_Left:
if (!textFieldFocused) {
root.keyboardNavigation = true;
root.selectedButton = 0;
event.accepted = true;
}
break;
case Qt.Key_Right:
if (!textFieldFocused) {
root.keyboardNavigation = true;
root.selectedButton = 1;
event.accepted = true;
}
break;
case Qt.Key_Return:
case Qt.Key_Enter:
if (root.selectedButton !== -1) {
root.selectButton();
} else {
root.confirmAndClose();
}
event.accepted = true;
break;
}
}
Column {
id: mainColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingL
spacing: 0
StyledText {
text: root.inputTitle
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
Item {
width: 1
height: Theme.spacingL
}
StyledText {
text: root.inputMessage
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
visible: root.inputMessage !== ""
}
Item {
width: 1
height: root.inputMessage !== "" ? Theme.spacingL : 0
visible: root.inputMessage !== ""
}
Rectangle {
width: parent.width
height: 40
radius: Theme.cornerRadius
color: Theme.surfaceVariantAlpha
border.color: textInput.activeFocus ? Theme.primary : "transparent"
border.width: textInput.activeFocus ? 1 : 0
TextInput {
id: textInput
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
verticalAlignment: TextInput.AlignVCenter
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
selectionColor: Theme.primary
selectedTextColor: Theme.primaryText
clip: true
text: root.inputText
onTextChanged: root.inputText = text
StyledText {
anchors.fill: parent
verticalAlignment: Text.AlignVCenter
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
text: root.inputPlaceholder
visible: textInput.text === "" && !textInput.activeFocus
}
}
}
Item {
width: 1
height: Theme.spacingL * 1.5
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
if (root.keyboardNavigation && root.selectedButton === 0) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
} else if (cancelButton.containsMouse) {
return Theme.surfacePressed;
} else {
return Theme.surfaceVariantAlpha;
}
}
border.color: (root.keyboardNavigation && root.selectedButton === 0) ? Theme.primary : "transparent"
border.width: (root.keyboardNavigation && root.selectedButton === 0) ? 1 : 0
StyledText {
text: root.cancelButtonText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.cancelAndClose()
}
}
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
const baseColor = root.confirmButtonColor;
if (root.keyboardNavigation && root.selectedButton === 1) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1);
} else if (confirmButton.containsMouse) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9);
} else {
return baseColor;
}
}
border.color: (root.keyboardNavigation && root.selectedButton === 1) ? "white" : "transparent"
border.width: (root.keyboardNavigation && root.selectedButton === 1) ? 1 : 0
StyledText {
text: root.confirmButtonText
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: confirmButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.confirmAndClose()
}
}
}
Item {
width: 1
height: Theme.spacingL
}
}
}
}
}
@@ -147,13 +147,6 @@ DankModal {
return "COLOR_PICKER_MODAL_OPEN_SUCCESS"; return "COLOR_PICKER_MODAL_OPEN_SUCCESS";
} }
function openColor(color: string): string {
root.selectedColor = Qt.color(color);
root.currentColor = Qt.color(color);
root.updateFromColor(Qt.color(color));
return open();
}
function close(): string { function close(): string {
root.hide(); root.hide();
return "COLOR_PICKER_MODAL_CLOSE_SUCCESS"; return "COLOR_PICKER_MODAL_CLOSE_SUCCESS";
@@ -207,12 +207,9 @@ Rectangle {
selectedActionIndex = 0; selectedActionIndex = 0;
} }
function cycleAction(reverse = false) { function cycleAction() {
if (actions.length > 0) { if (actions.length > 0) {
if (! reverse) selectedActionIndex = (selectedActionIndex + 1) % actions.length;
selectedActionIndex = (selectedActionIndex + 1) % actions.length;
else
selectedActionIndex = (selectedActionIndex - 1) % actions.length;
ensureSelectedVisible(); ensureSelectedVisible();
} }
} }
@@ -353,13 +353,10 @@ Item {
performSearch(); performSearch();
} }
function cycleMode(reverse = false) { function cycleMode() {
var modes = ["all", "apps", "files", "plugins"]; var modes = ["all", "apps", "files", "plugins"];
var currentIndex = modes.indexOf(searchMode); var currentIndex = modes.indexOf(searchMode);
if (!reverse) var nextIndex = (currentIndex + 1) % modes.length;
var nextIndex = (currentIndex + 1) % modes.length;
else
var nextIndex = (currentIndex - 1 + modes.length) % modes.length;
setMode(modes[nextIndex]); setMode(modes[nextIndex]);
} }
@@ -1009,7 +1006,9 @@ Item {
_applyHighlights(newSections, searchQuery); _applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections); flatModel = Scorer.flattenSections(newSections);
sections = newSections; sections = newSections;
selectedFlatIndex = getFirstItemIndex(); if (selectedFlatIndex >= flatModel.length) {
selectedFlatIndex = getFirstItemIndex();
}
updateSelectedItem(); updateSelectedItem();
}); });
} }
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
@@ -13,16 +14,24 @@ Item {
property bool spotlightOpen: false property bool spotlightOpen: false
property bool keyboardActive: false property bool keyboardActive: false
property bool contentVisible: false property bool contentVisible: false
readonly property bool launcherMotionVisible: Theme.isDirectionalEffect ? spotlightOpen : _motionActive
property var spotlightContent: launcherContentLoader.item property var spotlightContent: launcherContentLoader.item
property bool openedFromOverview: false property bool openedFromOverview: false
property bool isClosing: false property bool isClosing: false
property bool _windowEnabled: true
property bool _pendingInitialize: false property bool _pendingInitialize: false
property string _pendingQuery: "" property string _pendingQuery: ""
property string _pendingMode: "" property string _pendingMode: ""
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
// Animation state matches DankPopout/DankModal pattern
property bool animationsEnabled: true
property bool _motionActive: false
property real _frozenMotionX: 0
property real _frozenMotionY: 0
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property var effectiveScreen: launcherWindow.screen readonly property var effectiveScreen: contentWindow.screen
readonly property real screenWidth: effectiveScreen?.width ?? 1920 readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080 readonly property real screenHeight: effectiveScreen?.height ?? 1080
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1 readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
@@ -76,6 +85,34 @@ Item {
} }
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0 readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
// Shadow padding for the content window (render padding only, no motion padding)
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
readonly property real alignedX: Theme.snap(modalX, dpr)
readonly property real alignedY: Theme.snap(modalY, dpr)
// For directional/depth: window extends from screen top (content slides within)
// For standard: small window tightly around the modal + shadow padding
readonly property bool _needsExtendedWindow: Theme.isDirectionalEffect || Theme.isDepthEffect
// Content window geometry
readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr)
readonly property real _cwMarginTop: _needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr)
readonly property real _cwWidth: alignedWidth + shadowPad * 2
readonly property real _cwHeight: {
if (Theme.isDirectionalEffect)
return screenHeight + shadowPad;
if (Theme.isDepthEffect)
return alignedY + alignedHeight + shadowPad;
return alignedHeight + shadowPad * 2;
}
// Where the content container sits inside the content window
readonly property real _ccX: shadowPad
readonly property real _ccY: _needsExtendedWindow ? alignedY : shadowPad
signal dialogClosed signal dialogClosed
function _ensureContentLoadedAndInitialize(query, mode) { function _ensureContentLoadedAndInitialize(query, mode) {
@@ -95,7 +132,8 @@ Item {
if (!spotlightContent) if (!spotlightContent)
return; return;
contentVisible = true; contentVisible = true;
spotlightContent.searchField.forceActiveFocus(); // NOTE: forceActiveFocus() is deliberately NOT called here.
// It is deferred to after animation starts to avoid compositor IPC stalls.
if (spotlightContent.searchField) { if (spotlightContent.searchField) {
spotlightContent.searchField.text = query; spotlightContent.searchField.text = query;
@@ -128,40 +166,59 @@ Item {
} }
} }
function show() { function _openCommon(query, mode) {
closeCleanupTimer.stop(); closeCleanupTimer.stop();
isClosing = false; isClosing = false;
openedFromOverview = false; openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen(); // Disable animations so the snap is instant
if (focusedScreen) animationsEnabled = false;
launcherWindow.screen = focusedScreen;
spotlightOpen = true; // Freeze the collapsed offsets (they depend on height which could change)
keyboardActive = true; _frozenMotionX = contentContainer ? contentContainer.collapsedMotionX : 0;
_frozenMotionY = contentContainer ? contentContainer.collapsedMotionY : (Theme.isDirectionalEffect ? Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1) : -Theme.effectAnimOffset);
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) {
backgroundWindow.screen = focusedScreen;
contentWindow.screen = focusedScreen;
}
// _motionActive = false ensures motionX/Y snap to frozen collapsed position
_motionActive = false;
// Make windows visible but do NOT request keyboard focus yet
ModalManager.openModal(root); ModalManager.openModal(root);
spotlightOpen = true;
backgroundWindow.visible = true;
contentWindow.visible = true;
if (useHyprlandFocusGrab) if (useHyprlandFocusGrab)
focusGrab.active = true; focusGrab.active = true;
_ensureContentLoadedAndInitialize("", ""); // Load content and initialize (but no forceActiveFocus that's deferred)
_ensureContentLoadedAndInitialize(query || "", mode || "");
// Frame 1: enable animations and trigger enter motion
Qt.callLater(() => {
root.animationsEnabled = true;
root._motionActive = true;
// Frame 2: request keyboard focus + activate search field
// Double-deferred to avoid compositor IPC competing with animation frames
Qt.callLater(() => {
root.keyboardActive = true;
if (root.spotlightContent && root.spotlightContent.searchField)
root.spotlightContent.searchField.forceActiveFocus();
});
});
}
function show() {
_openCommon("", "");
} }
function showWithQuery(query) { function showWithQuery(query) {
closeCleanupTimer.stop(); _openCommon(query, "");
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize(query, "");
} }
function hide() { function hide() {
@@ -169,13 +226,17 @@ Item {
return; return;
openedFromOverview = false; openedFromOverview = false;
isClosing = true; isClosing = true;
contentVisible = false; // For directional effects, defer contentVisible=false so content stays rendered during exit slide
if (!Theme.isDirectionalEffect)
contentVisible = false;
// Trigger exit animation Behaviors will animate motionX/Y to frozen collapsed position
_motionActive = false;
keyboardActive = false; keyboardActive = false;
spotlightOpen = false; spotlightOpen = false;
focusGrab.active = false; focusGrab.active = false;
ModalManager.closeModal(root); ModalManager.closeModal(root);
closeCleanupTimer.start(); closeCleanupTimer.start();
} }
@@ -184,21 +245,7 @@ Item {
} }
function showWithMode(mode) { function showWithMode(mode) {
closeCleanupTimer.stop(); _openCommon("", mode);
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize("", mode);
} }
function toggleWithMode(mode) { function toggleWithMode(mode) {
@@ -219,10 +266,13 @@ Item {
Timer { Timer {
id: closeCleanupTimer id: closeCleanupTimer
interval: Theme.modalAnimationDuration + 50 interval: Theme.variantCloseInterval(Theme.modalAnimationDuration)
repeat: false repeat: false
onTriggered: { onTriggered: {
isClosing = false; isClosing = false;
contentVisible = false;
contentWindow.visible = false;
backgroundWindow.visible = false;
if (root.unloadContentOnClose) if (root.unloadContentOnClose)
launcherContentLoader.active = false; launcherContentLoader.active = false;
dialogClosed(); dialogClosed();
@@ -240,7 +290,7 @@ Item {
HyprlandFocusGrab { HyprlandFocusGrab {
id: focusGrab id: focusGrab
windows: [launcherWindow] windows: [contentWindow]
active: false active: false
onCleared: { onCleared: {
@@ -265,45 +315,53 @@ Item {
if (Quickshell.screens.length === 0) if (Quickshell.screens.length === 0)
return; return;
const screenName = launcherWindow.screen?.name; const screen = contentWindow.screen;
if (screenName) { const screenName = screen?.name;
let needsReset = !screen || !screenName;
if (!needsReset) {
needsReset = true;
for (let i = 0; i < Quickshell.screens.length; i++) { for (let i = 0; i < Quickshell.screens.length; i++) {
if (Quickshell.screens[i].name === screenName) if (Quickshell.screens[i].name === screenName) {
return; needsReset = false;
break;
}
} }
} }
if (spotlightOpen) if (!needsReset)
hide(); return;
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0]; const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
if (newScreen) if (!newScreen)
launcherWindow.screen = newScreen; return;
root._windowEnabled = false;
backgroundWindow.screen = newScreen;
contentWindow.screen = newScreen;
Qt.callLater(() => {
root._windowEnabled = true;
});
} }
} }
// Background window: fullscreen, handles darkening + click-to-dismiss
PanelWindow { PanelWindow {
id: launcherWindow id: backgroundWindow
visible: spotlightOpen || isClosing visible: false
color: "transparent" color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "dms:spotlight" WlrLayershell.namespace: "dms:spotlight:bg"
WlrLayershell.layer: { WlrLayershell.layer: WlrLayershell.Top
switch (Quickshell.env("DMS_MODAL_LAYER")) { WlrLayershell.exclusiveZone: -1
case "bottom": WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top; WlrLayershell.margins {
case "background": top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer."); bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
return WlrLayershell.Top; left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
case "overlay": right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
}
} }
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors { anchors {
top: true top: true
@@ -313,11 +371,11 @@ Item {
} }
mask: Region { mask: Region {
item: spotlightOpen ? fullScreenMask : null item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
} }
Item { Item {
id: fullScreenMask id: bgFullScreenMask
anchors.fill: parent anchors.fill: parent
} }
@@ -325,13 +383,14 @@ Item {
id: backgroundDarken id: backgroundDarken
anchors.fill: parent anchors.fill: parent
color: "black" color: "black"
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0 opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
visible: contentVisible || opacity > 0 visible: launcherMotionVisible || opacity > 0
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
DankAnim { DankAnim {
duration: Theme.modalAnimationDuration duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
} }
} }
} }
@@ -339,88 +398,240 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: spotlightOpen enabled: spotlightOpen
onClicked: mouse => { onClicked: root.hide()
var contentX = modalContainer.x; }
var contentY = modalContainer.y; }
var contentW = modalContainer.width;
var contentH = modalContainer.height;
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) { // Content window: SMALL, positioned with margins only renders the modal area
root.hide(); PanelWindow {
} id: contentWindow
visible: false
color: "transparent"
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
console.error("DankLauncherV2Modal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.error("DankLauncherV2Modal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
} }
} }
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
left: true
top: true
}
WlrLayershell.margins {
left: root._cwMarginLeft
top: root._cwMarginTop
}
implicitWidth: root._cwWidth
implicitHeight: root._cwHeight
mask: Region {
item: contentInputMask
}
Item { Item {
id: modalContainer id: contentInputMask
x: root.modalX visible: false
y: root.modalY x: contentContainer.x + contentWrapper.x
width: root.modalWidth y: contentContainer.y + contentWrapper.y
height: root.modalHeight width: root.alignedWidth
visible: contentVisible || opacity > 0 height: root.alignedHeight
opacity: contentVisible ? 1 : 0
scale: contentVisible ? 1 : 0.96
transformOrigin: Item.Center
Behavior on opacity {
DankAnim {
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Behavior on scale {
DankAnim {
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
ElevationShadow {
id: launcherShadowLayer
anchors.fill: parent
level: Theme.elevationLevel3
fallbackOffset: 6
targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
targetRadius: root.cornerRadius
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
}
MouseArea {
anchors.fill: parent
onPressed: mouse => mouse.accepted = true
}
FocusScope {
anchors.fill: parent
focus: keyboardActive
Loader {
id: launcherContentLoader
anchors.fill: parent
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
asynchronous: false
sourceComponent: LauncherContent {
focus: true
parentModal: root
}
onLoaded: {
if (root._pendingInitialize) {
root._initializeAndShow(root._pendingQuery, root._pendingMode);
root._pendingInitialize = false;
}
}
}
Keys.onEscapePressed: event => {
root.hide();
event.accepted = true;
}
}
} }
}
Item {
id: contentContainer
// For directional/depth: contentContainer is at alignedY from window top (window starts at screen top)
// For standard: contentContainer is at shadowPad from window top (window starts near modal)
x: root._ccX
y: root._ccY
width: root.alignedWidth
height: root.alignedHeight
readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1
readonly property bool dockTop: dockEdge === 0
readonly property bool dockBottom: dockEdge === 1
readonly property bool dockLeft: dockEdge === 2
readonly property bool dockRight: dockEdge === 3
readonly property real dockThickness: typeof SettingsData !== "undefined" && SettingsData.showDock ? Theme.px(SettingsData.dockIconSize + (SettingsData.dockMargin * 2) + SettingsData.dockSpacing + 8, root.dpr) : Theme.px(60, root.dpr)
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real collapsedMotionX: {
if (directionalEffect) {
if (dockLeft)
return -(root._ccX + root.alignedWidth + Theme.effectAnimOffset);
if (dockRight)
return root.screenWidth - root._ccX + Theme.effectAnimOffset;
}
if (depthEffect)
return Theme.effectAnimOffset * 0.25;
return 0;
}
readonly property real collapsedMotionY: {
if (directionalEffect) {
if (dockTop)
return -(root._ccY + root.alignedHeight + Theme.effectAnimOffset);
if (dockBottom)
return root.screenHeight - root._ccY + root.shadowPad + Theme.effectAnimOffset;
return 0;
}
if (depthEffect)
return -Math.max(Theme.effectAnimOffset * 0.85, 34);
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
}
// animX/animY are Behavior-animated DankPopout pattern
property real animX: 0
property real animY: 0
property real scaleValue: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed)
Component.onCompleted: {
animX = Theme.snap(root._motionActive ? 0 : collapsedMotionX, root.dpr);
animY = Theme.snap(root._motionActive ? 0 : collapsedMotionY, root.dpr);
scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
}
Connections {
target: root
function on_MotionActiveChanged() {
contentContainer.animX = Theme.snap(root._motionActive ? 0 : root._frozenMotionX, root.dpr);
contentContainer.animY = Theme.snap(root._motionActive ? 0 : root._frozenMotionY, root.dpr);
contentContainer.scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
}
}
Behavior on animX {
enabled: root.animationsEnabled
DankAnim {
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
DankAnim {
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2))
DankAnim {
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Item {
id: directionalClipMask
readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0
readonly property real clipOversize: 2000
clip: shouldClip
x: shouldClip ? (contentContainer.dockRight ? -clipOversize : (contentContainer.dockLeft ? contentContainer.dockThickness - root._ccX : -clipOversize)) : 0
y: shouldClip ? (contentContainer.dockBottom ? -clipOversize : (contentContainer.dockTop ? contentContainer.dockThickness - root._ccY : -clipOversize)) : 0
width: shouldClip ? parent.width + clipOversize + (contentContainer.dockRight ? (root.screenWidth - contentContainer.dockThickness - root._ccX - parent.width) : (contentContainer.dockLeft ? clipOversize : clipOversize)) : parent.width
height: shouldClip ? parent.height + clipOversize + (contentContainer.dockBottom ? (root.screenHeight - contentContainer.dockThickness - root._ccY - parent.height) : (contentContainer.dockTop ? clipOversize : clipOversize)) : parent.height
Item {
id: aligner
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
width: contentContainer.width
height: contentContainer.height
// Shadow mirrors contentWrapper position/scale/opacity
ElevationShadow {
id: launcherShadowLayer
width: parent.width
height: parent.height
opacity: contentWrapper.opacity
scale: contentWrapper.scale
x: contentWrapper.x
y: contentWrapper.y
level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset
targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
targetRadius: root.cornerRadius
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
}
// contentWrapper moves inside static contentContainer DankPopout pattern
Item {
id: contentWrapper
width: parent.width
height: parent.height
opacity: Theme.isDirectionalEffect ? 1 : (launcherMotionVisible ? 1 : 0)
visible: opacity > 0
scale: contentContainer.scaleValue
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
DankAnim {
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
MouseArea {
anchors.fill: parent
onPressed: mouse => mouse.accepted = true
}
FocusScope {
anchors.fill: parent
focus: keyboardActive
Loader {
id: launcherContentLoader
anchors.fill: parent
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
asynchronous: false
sourceComponent: LauncherContent {
focus: true
parentModal: root
}
onLoaded: {
if (root._pendingInitialize) {
root._initializeAndShow(root._pendingQuery, root._pendingMode);
root._pendingInitialize = false;
}
}
}
Keys.onEscapePressed: event => {
root.hide();
event.accepted = true;
}
}
} // contentWrapper
} // aligner
} // directionalClipMask
} // contentContainer
} // PanelWindow
} }
@@ -41,7 +41,6 @@ FocusScope {
editCommentField.text = existing?.comment || ""; editCommentField.text = existing?.comment || "";
editEnvVarsField.text = existing?.envVars || ""; editEnvVarsField.text = existing?.envVars || "";
editExtraFlagsField.text = existing?.extraFlags || ""; editExtraFlagsField.text = existing?.extraFlags || "";
editDgpuToggle.checked = existing?.launchOnDgpu || false;
editMode = true; editMode = true;
Qt.callLater(() => editNameField.forceActiveFocus()); Qt.callLater(() => editNameField.forceActiveFocus());
} }
@@ -65,8 +64,6 @@ FocusScope {
override.envVars = editEnvVarsField.text.trim(); override.envVars = editEnvVarsField.text.trim();
if (editExtraFlagsField.text.trim()) if (editExtraFlagsField.text.trim())
override.extraFlags = editExtraFlagsField.text.trim(); override.extraFlags = editExtraFlagsField.text.trim();
if (editDgpuToggle.checked)
override.launchOnDgpu = true;
SessionData.setAppOverride(editAppId, override); SessionData.setAppOverride(editAppId, override);
closeEditMode(); closeEditMode();
} }
@@ -89,7 +86,7 @@ FocusScope {
Controller { Controller {
id: controller id: controller
active: root.parentModal?.spotlightOpen ?? true active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
viewModeContext: root.viewModeContext viewModeContext: root.viewModeContext
onItemExecuted: { onItemExecuted: {
@@ -161,10 +158,6 @@ FocusScope {
controller.selectPageUp(8); controller.selectPageUp(8);
return; return;
case Qt.Key_Right: case Qt.Key_Right:
if (hasCtrl) {
controller.cycleMode();
return;
}
if (controller.getCurrentSectionViewMode() !== "list") { if (controller.getCurrentSectionViewMode() !== "list") {
controller.selectRight(); controller.selectRight();
return; return;
@@ -172,25 +165,12 @@ FocusScope {
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_Left: case Qt.Key_Left:
if (hasCtrl) {
const reverse = true;
controller.cycleMode(reverse);
return;
}
if (controller.getCurrentSectionViewMode() !== "list") { if (controller.getCurrentSectionViewMode() !== "list") {
controller.selectLeft(); controller.selectLeft();
return; return;
} }
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_H:
if (hasCtrl) {
const reverse = true;
controller.cycleMode(reverse);
return;
}
event.accepted = false;
return;
case Qt.Key_J: case Qt.Key_J:
if (hasCtrl) { if (hasCtrl) {
controller.selectNext(); controller.selectNext();
@@ -205,13 +185,6 @@ FocusScope {
} }
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_L:
if (hasCtrl) {
controller.cycleMode();
return;
}
event.accepted = false;
return;
case Qt.Key_N: case Qt.Key_N:
if (hasCtrl) { if (hasCtrl) {
controller.selectNextSection(); controller.selectNextSection();
@@ -227,19 +200,13 @@ FocusScope {
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_Tab: case Qt.Key_Tab:
if (hasCtrl && actionPanel.hasActions) { if (actionPanel.hasActions) {
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show(); actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
return;
} }
controller.selectNext();
return; return;
case Qt.Key_Backtab: case Qt.Key_Backtab:
if (hasCtrl && actionPanel.expanded) { if (actionPanel.expanded)
const reverse = true; actionPanel.hide();
actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show();
return;
}
controller.selectPrevious();
return; return;
case Qt.Key_Return: case Qt.Key_Return:
case Qt.Key_Enter: case Qt.Key_Enter:
@@ -421,7 +388,7 @@ FocusScope {
StyledText { StyledText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Ctrl-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
visible: actionPanel.hasActions visible: actionPanel.hasActions
@@ -495,7 +462,7 @@ FocusScope {
showClearButton: true showClearButton: true
textColor: Theme.surfaceText textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
enabled: root.parentModal ? root.parentModal.spotlightOpen : true enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
placeholderText: "" placeholderText: ""
ignoreUpDownKeys: true ignoreUpDownKeys: true
ignoreTabKeys: true ignoreTabKeys: true
@@ -729,7 +696,13 @@ FocusScope {
Item { Item {
width: parent.width width: parent.width
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2) height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
opacity: root.parentModal?.isClosing ? 0 : 1 opacity: {
if (!root.parentModal)
return 1;
if (Theme.isDirectionalEffect && root.parentModal.isClosing)
return 1;
return root.parentModal.isClosing ? 0 : 1;
}
ResultsList { ResultsList {
id: resultsList id: resultsList
@@ -973,15 +946,6 @@ FocusScope {
keyNavigationBacktab: editEnvVarsField keyNavigationBacktab: editEnvVarsField
} }
} }
DankToggle {
id: editDgpuToggle
width: parent.width
text: I18n.tr("Launch on dGPU by default")
visible: SessionService.nvidiaCommand.length > 0
checked: false
onToggled: checked => editDgpuToggle.checked = checked
}
} }
} }
@@ -468,7 +468,7 @@ Item {
switch (mode) { switch (mode) {
case "files": case "files":
if (!DSearchService.dsearchAvailable) if (!DSearchService.dsearchAvailable)
return I18n.tr("File search requires dsearch\nInstall from github.com/AvengeMedia/danksearch"); return I18n.tr("File search requires dsearch\nInstall from github.com/morelazers/dsearch");
if (!hasQuery) if (!hasQuery)
return I18n.tr("Type to search files"); return I18n.tr("Type to search files");
if (root.controller.searchQuery.length < 2) if (root.controller.searchQuery.length < 2)
-633
View File
@@ -1,633 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: muxModal
layerNamespace: "dms:mux"
property int selectedIndex: -1
property string searchText: ""
property var filteredSessions: []
function updateFilteredSessions() {
var filtered = []
var lowerSearch = searchText.trim().toLowerCase()
for (var i = 0; i < MuxService.sessions.length; i++) {
var session = MuxService.sessions[i]
if (lowerSearch.length > 0 && !session.name.toLowerCase().includes(lowerSearch))
continue
filtered.push(session)
}
filteredSessions = filtered
if (selectedIndex >= filteredSessions.length) {
selectedIndex = Math.max(0, filteredSessions.length - 1)
}
}
onSearchTextChanged: updateFilteredSessions()
Connections {
target: MuxService
function onSessionsChanged() {
updateFilteredSessions()
}
}
HyprlandFocusGrab {
id: grab
windows: [muxModal.contentWindow]
active: CompositorService.isHyprland && muxModal.shouldHaveFocus
}
function toggle() {
if (shouldBeVisible) {
hide()
} else {
show()
}
}
function show() {
open()
selectedIndex = -1
searchText = ""
MuxService.refreshSessions()
shouldHaveFocus = true
Qt.callLater(() => {
if (muxPanel && muxPanel.searchField) {
muxPanel.searchField.forceActiveFocus();
}
})
}
function hide() {
close()
selectedIndex = -1
searchText = ""
}
function attachToSession(name) {
MuxService.attachToSession(name)
hide()
}
function renameSession(name) {
inputModal.showWithOptions({
title: I18n.tr("Rename Session"),
message: I18n.tr("Enter a new name for session \"%1\"").arg(name),
initialText: name,
onConfirm: function (newName) {
MuxService.renameSession(name, newName)
}
})
}
function killSession(name) {
confirmModal.showWithOptions({
title: I18n.tr("Kill Session"),
message: I18n.tr("Are you sure you want to kill session \"%1\"?").arg(name),
confirmText: I18n.tr("Kill"),
confirmColor: Theme.primary,
onConfirm: function () {
MuxService.killSession(name)
}
})
}
function createNewSession() {
inputModal.showWithOptions({
title: I18n.tr("New Session"),
message: I18n.tr("Please write a name for your new %1 session").arg(MuxService.displayName),
onConfirm: function (name) {
MuxService.createSession(name)
hide()
}
})
}
function selectNext() {
selectedIndex = Math.min(selectedIndex + 1, filteredSessions.length - 1)
}
function selectPrevious() {
selectedIndex = Math.max(selectedIndex - 1, -1)
}
function activateSelected() {
if (selectedIndex === -1) {
createNewSession()
} else if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
attachToSession(filteredSessions[selectedIndex].name)
}
}
visible: false
modalWidth: 600
modalHeight: 600
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
keepContentLoaded: true
onBackgroundClicked: hide()
Timer {
interval: 3000
running: muxModal.shouldBeVisible
repeat: true
onTriggered: MuxService.refreshSessions()
}
IpcHandler {
function open(): string {
muxModal.show()
return "MUX_OPEN_SUCCESS"
}
function close(): string {
muxModal.hide()
return "MUX_CLOSE_SUCCESS"
}
function toggle(): string {
muxModal.toggle()
return "MUX_TOGGLE_SUCCESS"
}
target: "mux"
}
// Backwards compatibility
IpcHandler {
function open(): string {
muxModal.show()
return "TMUX_OPEN_SUCCESS"
}
function close(): string {
muxModal.hide()
return "TMUX_CLOSE_SUCCESS"
}
function toggle(): string {
muxModal.toggle()
return "TMUX_TOGGLE_SUCCESS"
}
target: "tmux"
}
InputModal {
id: inputModal
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
muxModal.shouldHaveFocus = false;
muxModal.contentWindow.visible = false;
return;
}
if (muxModal.shouldBeVisible) {
muxModal.contentWindow.visible = true;
}
Qt.callLater(function () {
if (!muxModal.shouldBeVisible) {
return;
}
muxModal.shouldHaveFocus = true;
muxModal.modalFocusScope.forceActiveFocus();
if (muxPanel.searchField) {
muxPanel.searchField.forceActiveFocus();
}
});
}
}
ConfirmModal {
id: confirmModal
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
muxModal.shouldHaveFocus = false;
muxModal.contentWindow.visible = false;
return;
}
if (muxModal.shouldBeVisible) {
muxModal.contentWindow.visible = true;
}
Qt.callLater(function () {
if (!muxModal.shouldBeVisible) {
return;
}
muxModal.shouldHaveFocus = true;
muxModal.modalFocusScope.forceActiveFocus();
if (muxPanel.searchField) {
muxPanel.searchField.forceActiveFocus();
}
});
}
}
directContent: Item {
id: muxPanel
clip: false
property alias searchField: searchField
Keys.onPressed: event => {
if ((event.key === Qt.Key_J && (event.modifiers & Qt.ControlModifier)) ||
(event.key === Qt.Key_Down)) {
selectNext()
event.accepted = true
} else if ((event.key === Qt.Key_K && (event.modifiers & Qt.ControlModifier)) ||
(event.key === Qt.Key_Up)) {
selectPrevious()
event.accepted = true
} else if (event.key === Qt.Key_N && (event.modifiers & Qt.ControlModifier)) {
createNewSession()
event.accepted = true
} else if (event.key === Qt.Key_R && (event.modifiers & Qt.ControlModifier)) {
if (MuxService.supportsRename && selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
renameSession(filteredSessions[selectedIndex].name)
}
event.accepted = true
} else if (event.key === Qt.Key_D && (event.modifiers & Qt.ControlModifier)) {
if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
killSession(filteredSessions[selectedIndex].name)
}
event.accepted = true
} else if (event.key === Qt.Key_Escape) {
hide()
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
activateSelected()
event.accepted = true
}
}
Column {
width: parent.width - Theme.spacingM * 2
height: parent.height - Theme.spacingM * 2
x: Theme.spacingM
y: Theme.spacingM
spacing: Theme.spacingS
// Header
Item {
width: parent.width
height: 40
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("%1 Sessions").arg(MuxService.displayName)
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: {
const total = MuxService.sessions.length;
const filtered = muxModal.filteredSessions.length;
const activePart = total === 1
? I18n.tr("%1 active session").arg(total)
: I18n.tr("%1 active sessions").arg(total);
const filteredPart = filtered === 1
? I18n.tr("%1 filtered").arg(filtered)
: I18n.tr("%1 filtered").arg(filtered);
return activePart + ", " + filteredPart;
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
}
// Search field
DankTextField {
id: searchField
width: parent.width
height: 48
cornerRadius: Theme.cornerRadius
backgroundColor: Theme.surfaceContainerHigh
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
font.pixelSize: Theme.fontSizeMedium
placeholderText: I18n.tr("Search sessions...")
keyForwardTargets: [muxPanel]
onTextEdited: {
muxModal.searchText = text
muxModal.selectedIndex = 0
}
}
// New Session Button
Rectangle {
width: parent.width
height: 56
radius: Theme.cornerRadius
color: muxModal.selectedIndex === -1 ? Theme.primaryContainer :
(newMouse.containsMouse ? Theme.surfaceContainerHigh : Theme.surfaceContainer)
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
radius: 20
color: Theme.primaryContainer
DankIcon {
anchors.centerIn: parent
name: "add"
size: Theme.iconSize
color: Theme.primary
}
}
Column {
Layout.fillWidth: true
spacing: 2
StyledText {
text: I18n.tr("New Session")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Create a new %1 session (n)").arg(MuxService.displayName)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
MouseArea {
id: newMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: muxModal.createNewSession()
}
}
// Sessions List
Rectangle {
width: parent.width
height: parent.height - 88 - 48 - shortcutsBar.height - Theme.spacingS * 3
radius: Theme.cornerRadius
color: "transparent"
ScrollView {
anchors.fill: parent
clip: true
Column {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: ScriptModel {
values: muxModal.filteredSessions
}
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 64
radius: Theme.cornerRadius
color: muxModal.selectedIndex === index ? Theme.primaryContainer :
(sessionMouse.containsMouse ? Theme.surfaceContainerHigh : "transparent")
MouseArea {
id: sessionMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: muxModal.attachToSession(modelData.name)
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
// Avatar
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
radius: 20
color: modelData.attached ? Theme.primaryContainer : Theme.surfaceContainerHigh
StyledText {
anchors.centerIn: parent
text: modelData.name.charAt(0).toUpperCase()
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: modelData.attached ? Theme.primary : Theme.surfaceText
}
}
// Info
Column {
Layout.fillWidth: true
spacing: 2
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
}
StyledText {
text: {
var parts = []
if (modelData.windows !== "N/A")
parts.push(modelData.windows === 1
? I18n.tr("%1 window").arg(modelData.windows)
: I18n.tr("%1 windows").arg(modelData.windows))
parts.push(modelData.attached ? I18n.tr("attached") : I18n.tr("detached"))
return parts.join(" \u2022 ")
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
// Rename button (tmux only)
Rectangle {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
radius: 18
visible: MuxService.supportsRename
color: renameMouse.containsMouse ? Theme.surfaceContainerHighest : "transparent"
DankIcon {
anchors.centerIn: parent
name: "edit"
size: Theme.iconSizeSmall
color: renameMouse.containsMouse ? Theme.primary : Theme.surfaceVariantText
}
MouseArea {
id: renameMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: muxModal.renameSession(modelData.name)
}
}
// Delete button
Rectangle {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
radius: 18
color: deleteMouse.containsMouse ? Theme.errorContainer : "transparent"
DankIcon {
anchors.centerIn: parent
name: "delete"
size: Theme.iconSizeSmall
color: deleteMouse.containsMouse ? Theme.error : Theme.surfaceVariantText
}
MouseArea {
id: deleteMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
muxModal.killSession(modelData.name)
}
}
}
}
}
}
// Empty state
Item {
width: parent.width
height: muxModal.filteredSessions.length === 0 ? 200 : 0
visible: muxModal.filteredSessions.length === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
name: muxModal.searchText.length > 0 ? "search_off" : "terminal"
size: 48
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: muxModal.searchText.length > 0 ? I18n.tr("No sessions found") : I18n.tr("No active %1 sessions").arg(MuxService.displayName)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: muxModal.searchText.length > 0 ? I18n.tr("Try a different search") : I18n.tr("Press 'n' or click 'New Session' to create one")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
}
// Shortcuts bar
Row {
id: shortcutsBar
width: parent.width
spacing: Theme.spacingM
bottomPadding: Theme.spacingS
Repeater {
model: {
var shortcuts = [
{ key: "↑↓", label: I18n.tr("Navigate") },
{ key: "↵", label: I18n.tr("Attach") },
{ key: "^N", label: I18n.tr("New") },
{ key: "^D", label: I18n.tr("Kill") },
{ key: "Esc", label: I18n.tr("Close") }
]
if (MuxService.supportsRename)
shortcuts.splice(3, 0, { key: "^R", label: I18n.tr("Rename") })
return shortcuts
}
delegate: Row {
required property var modelData
spacing: 4
Rectangle {
width: keyText.width + Theme.spacingS
height: keyText.height + 4
radius: 4
color: Theme.surfaceContainerHighest
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: keyText
anchors.centerIn: parent
text: modelData.key
font.pixelSize: Theme.fontSizeSmall - 1
font.weight: Font.Medium
color: Theme.surfaceVariantText
}
}
StyledText {
text: modelData.label
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
+2 -2
View File
@@ -66,7 +66,7 @@ DankModal {
} }
StyledText { StyledText {
text: I18n.tr("Details for \"%1\"").arg(networkSSID) text: `Details for "${networkSSID}"`
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium color: Theme.surfaceTextMedium
width: parent.width width: parent.width
@@ -102,7 +102,7 @@ DankModal {
id: detailsText id: detailsText
width: parent.width width: parent.width
text: NetworkService.networkInfoDetails && NetworkService.networkInfoDetails.replace(/\\n/g, '\n') || I18n.tr("No information available") text: NetworkService.networkInfoDetails && NetworkService.networkInfoDetails.replace(/\\n/g, '\n') || "No information available"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
+2 -2
View File
@@ -66,7 +66,7 @@ DankModal {
} }
StyledText { StyledText {
text: I18n.tr("Details for \"%1\"").arg(networkSSID) text: `Details for "${networkID}"`
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium color: Theme.surfaceTextMedium
width: parent.width width: parent.width
@@ -102,7 +102,7 @@ DankModal {
id: detailsText id: detailsText
width: parent.width width: parent.width
text: NetworkService.networkWiredInfoDetails && NetworkService.networkWiredInfoDetails.replace(/\\n/g, '\n') || I18n.tr("No information available") text: NetworkService.networkWiredInfoDetails && NetworkService.networkWiredInfoDetails.replace(/\\n/g, '\n') || "No information available"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
+30 -114
View File
@@ -1,6 +1,5 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -12,45 +11,8 @@ FloatingWindow {
property string passwordInput: "" property string passwordInput: ""
property var currentFlow: PolkitService.agent?.flow property var currentFlow: PolkitService.agent?.flow
property bool isLoading: false property bool isLoading: false
property bool awaitingFprintForPassword: false
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
property string polkitEtcPamText: ""
property string polkitLibPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
property string passwordAuthPamText: ""
readonly property bool polkitPamHasFprint: {
const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText;
if (!polkitText)
return false;
return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"));
}
function stripPamComment(line) {
if (!line)
return "";
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#"))
return "";
const hashIdx = trimmed.indexOf("#");
if (hashIdx >= 0)
return trimmed.substring(0, hashIdx).trim();
return trimmed;
}
function pamModuleEnabled(pamText, moduleName) {
if (!pamText || !moduleName)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (line && line.includes(moduleName))
return true;
}
return false;
}
function focusPasswordField() { function focusPasswordField() {
passwordField.forceActiveFocus(); passwordField.forceActiveFocus();
} }
@@ -58,7 +20,6 @@ FloatingWindow {
function show() { function show() {
passwordInput = ""; passwordInput = "";
isLoading = false; isLoading = false;
awaitingFprintForPassword = false;
visible = true; visible = true;
Qt.callLater(focusPasswordField); Qt.callLater(focusPasswordField);
} }
@@ -67,27 +28,17 @@ FloatingWindow {
visible = false; visible = false;
} }
function _commitSubmit() {
isLoading = true;
awaitingFprintForPassword = false;
currentFlow.submit(passwordInput);
passwordInput = "";
}
function submitAuth() { function submitAuth() {
if (!currentFlow || isLoading) if (!currentFlow || isLoading)
return; return;
if (!currentFlow.isResponseRequired) { isLoading = true;
awaitingFprintForPassword = true; currentFlow.submit(passwordInput);
return; passwordInput = "";
}
_commitSubmit();
} }
function cancelAuth() { function cancelAuth() {
if (isLoading) if (isLoading)
return; return;
awaitingFprintForPassword = false;
if (currentFlow) { if (currentFlow) {
currentFlow.cancelAuthenticationRequest(); currentFlow.cancelAuthenticationRequest();
return; return;
@@ -109,7 +60,6 @@ FloatingWindow {
} }
passwordInput = ""; passwordInput = "";
isLoading = false; isLoading = false;
awaitingFprintForPassword = false;
} }
Connections { Connections {
@@ -133,11 +83,6 @@ FloatingWindow {
function onIsResponseRequiredChanged() { function onIsResponseRequiredChanged() {
if (!currentFlow.isResponseRequired) if (!currentFlow.isResponseRequired)
return; return;
if (awaitingFprintForPassword && passwordInput !== "") {
_commitSubmit();
return;
}
awaitingFprintForPassword = false;
isLoading = false; isLoading = false;
passwordInput = ""; passwordInput = "";
passwordField.forceActiveFocus(); passwordField.forceActiveFocus();
@@ -156,41 +101,6 @@ FloatingWindow {
} }
} }
FileView {
path: "/etc/pam.d/polkit-1"
printErrors: false
onLoaded: root.polkitEtcPamText = text()
onLoadFailed: root.polkitEtcPamText = ""
}
FileView {
path: "/usr/lib/pam.d/polkit-1"
printErrors: false
onLoaded: root.polkitLibPamText = text()
onLoadFailed: root.polkitLibPamText = ""
}
FileView {
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: root.systemAuthPamText = text()
onLoadFailed: root.systemAuthPamText = ""
}
FileView {
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: root.commonAuthPamText = text()
onLoadFailed: root.commonAuthPamText = ""
}
FileView {
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: root.passwordAuthPamText = text()
onLoadFailed: root.passwordAuthPamText = ""
}
FocusScope { FocusScope {
id: contentFocusScope id: contentFocusScope
@@ -295,30 +205,36 @@ FloatingWindow {
visible: text !== "" visible: text !== ""
} }
DankTextField { Rectangle {
id: passwordField
width: parent.width width: parent.width
height: inputFieldHeight height: inputFieldHeight
backgroundColor: Theme.surfaceHover radius: Theme.cornerRadius
normalBorderColor: Theme.outlineStrong color: Theme.surfaceHover
focusedBorderColor: Theme.primary border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong
borderWidth: 1 border.width: passwordField.activeFocus ? 2 : 1
focusedBorderWidth: 2
leftIconName: polkitPamHasFprint ? "fingerprint" : ""
leftIconSize: 20
leftIconColor: Theme.primary
leftIconFocusedColor: Theme.primary
opacity: isLoading ? 0.5 : 1 opacity: isLoading ? 0.5 : 1
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText MouseArea {
text: passwordInput anchors.fill: parent
showPasswordToggle: !(currentFlow?.responseVisible ?? false) enabled: !isLoading
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password onClicked: passwordField.forceActiveFocus()
placeholderText: "" }
enabled: !isLoading
onTextEdited: passwordInput = text DankTextField {
onAccepted: submitAuth() id: passwordField
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: passwordInput
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
placeholderText: ""
backgroundColor: "transparent"
enabled: !isLoading
onTextEdited: passwordInput = text
onAccepted: submitAuth()
}
} }
StyledText { StyledText {
+2 -32
View File
@@ -241,21 +241,6 @@ FocusScope {
} }
} }
Loader {
id: greeterLoader
anchors.fill: parent
active: root.currentIndex === 31
visible: active
focus: active
sourceComponent: GreeterTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader { Loader {
id: pluginsLoader id: pluginsLoader
anchors.fill: parent anchors.fill: parent
@@ -485,7 +470,7 @@ FocusScope {
onActiveChanged: { onActiveChanged: {
if (active && item) if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
} }
} }
@@ -500,22 +485,7 @@ FocusScope {
onActiveChanged: { onActiveChanged: {
if (active && item) if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: muxLoader
anchors.fill: parent
active: root.currentIndex === 30
visible: active
focus: active
sourceComponent: MuxTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
} }
} }
} }
@@ -266,12 +266,6 @@ Rectangle {
"tabIndex": 8, "tabIndex": 8,
"cupsOnly": true "cupsOnly": true
}, },
{
"id": "multiplexers",
"text": I18n.tr("Multiplexers"),
"icon": "terminal",
"tabIndex": 30
},
{ {
"id": "window_rules", "id": "window_rules",
"text": I18n.tr("Window Rules"), "text": I18n.tr("Window Rules"),
@@ -293,12 +287,6 @@ Rectangle {
"icon": "lock", "icon": "lock",
"tabIndex": 11 "tabIndex": 11
}, },
{
"id": "greeter",
"text": I18n.tr("Greeter"),
"icon": "login",
"tabIndex": 31
},
{ {
"id": "power_sleep", "id": "power_sleep",
"text": I18n.tr("Power & Sleep"), "text": I18n.tr("Power & Sleep"),
@@ -370,7 +358,6 @@ Rectangle {
if (_collapsedIds.indexOf(marker) < 0) if (_collapsedIds.indexOf(marker) < 0)
_collapsedIds = _collapsedIds + id + ","; _collapsedIds = _collapsedIds + id + ",";
} }
SessionData.setSettingsSidebarState(_expandedIds, _collapsedIds);
} }
function _setAutoExpanded(id, value) { function _setAutoExpanded(id, value) {
@@ -539,11 +526,6 @@ Rectangle {
color: Theme.surfaceContainer color: Theme.surfaceContainer
radius: Theme.cornerRadius radius: Theme.cornerRadius
Component.onCompleted: {
root._expandedIds = SessionData.settingsSidebarExpandedIds;
root._collapsedIds = SessionData.settingsSidebarCollapsedIds;
}
StyledTextMetrics { StyledTextMetrics {
id: __m1 id: __m1
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
@@ -8,9 +8,6 @@ DankPopout {
layerNamespace: "dms:app-launcher" layerNamespace: "dms:app-launcher"
readonly property real screenWidth: screen?.width ?? 1920
readonly property real screenHeight: screen?.height ?? 1080
property string _pendingMode: "" property string _pendingMode: ""
property string _pendingQuery: "" property string _pendingQuery: ""
@@ -44,35 +41,8 @@ DankPopout {
openWithQuery(query); openWithQuery(query);
} }
readonly property int _baseWidth: { popupWidth: 560
switch (SettingsData.dankLauncherV2Size) { popupHeight: 640
case "micro":
return 500;
case "medium":
return 720;
case "large":
return 860;
default:
return 620;
}
}
readonly property int _baseHeight: {
switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 480;
case "medium":
return 720;
case "large":
return 860;
default:
return 600;
}
}
popupWidth: Math.min(_baseWidth, screenWidth - 100)
popupHeight: Math.min(_baseHeight, screenHeight - 100)
triggerWidth: 40 triggerWidth: 40
positioning: "" positioning: ""
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
@@ -136,7 +106,7 @@ DankPopout {
QtObject { QtObject {
id: modalAdapter id: modalAdapter
property bool spotlightOpen: appDrawerPopout.shouldBeVisible property bool spotlightOpen: appDrawerPopout.shouldBeVisible
property bool isClosing: false property bool isClosing: appDrawerPopout.isClosing
function hide() { function hide() {
appDrawerPopout.close(); appDrawerPopout.close();
@@ -86,7 +86,7 @@ Variants {
Component.onCompleted: { Component.onCompleted: {
if (typeof blurWallpaperWindow.updatesEnabled !== "undefined") if (typeof blurWallpaperWindow.updatesEnabled !== "undefined")
blurWallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading); blurWallpaperWindow.updatesEnabled = Qt.binding(() => root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
isInitialized = true; isInitialized = true;
} }
@@ -167,8 +167,6 @@ Variants {
transitionAnimation.stop(); transitionAnimation.stop();
root.transitionProgress = 0; root.transitionProgress = 0;
root.effectActive = false; root.effectActive = false;
root._renderSettling = true;
renderSettleTimer.restart();
currentWallpaper.source = nextWallpaper.source; currentWallpaper.source = nextWallpaper.source;
nextWallpaper.source = ""; nextWallpaper.source = "";
} }
@@ -177,9 +175,6 @@ Variants {
return; return;
} }
root._renderSettling = true;
renderSettleTimer.restart();
nextWallpaper.source = newPath; nextWallpaper.source = newPath;
if (nextWallpaper.status === Image.Ready) if (nextWallpaper.status === Image.Ready)
@@ -12,7 +12,7 @@ DankPopout {
id: root id: root
layerNamespace: "dms:control-center" layerNamespace: "dms:control-center"
fullHeightSurface: true fullHeightSurface: false
property string expandedSection: "" property string expandedSection: ""
property var triggerScreen: null property var triggerScreen: null
@@ -70,16 +70,6 @@ DankPopout {
backgroundInteractive: !anyModalOpen backgroundInteractive: !anyModalOpen
onCredentialsPromptOpenChanged: {
if (credentialsPromptOpen && shouldBeVisible)
close();
}
onPolkitModalOpenChanged: {
if (polkitModalOpen && shouldBeVisible)
close();
}
customKeyboardFocus: { customKeyboardFocus: {
if (!shouldBeVisible) if (!shouldBeVisible)
return WlrKeyboardFocus.None; return WlrKeyboardFocus.None;
@@ -136,9 +126,11 @@ DankPopout {
z: 5000 z: 5000
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: 200 duration: Theme.shortDuration
easing.type: Easing.OutCubic easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
} }
@@ -1117,6 +1117,7 @@ Item {
if (!notificationCenterLoader.item) { if (!notificationCenterLoader.item) {
return; return;
} }
notificationCenterLoader.item.triggerScreen = barWindow.screen;
const effectiveBarConfig = topBarContent.barConfig; const effectiveBarConfig = topBarContent.barConfig;
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
if (notificationCenterLoader.item.setBarContext) { if (notificationCenterLoader.item.setBarContext) {
+4 -5
View File
@@ -289,7 +289,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screenName) if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
return false; return false;
return true; return true;
}); });
@@ -312,7 +312,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screenName) if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
return false; return false;
return true; return true;
}); });
@@ -336,7 +336,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screenName) if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
return false; return false;
return true; return true;
}); });
@@ -360,7 +360,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screenName) if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
return false; return false;
return true; return true;
}); });
@@ -686,7 +686,6 @@ PanelWindow {
onHasActivePopoutChanged: evaluateReveal() onHasActivePopoutChanged: evaluateReveal()
function updateActivePopoutState() { function updateActivePopoutState() {
if (!barWindow.screen) return;
const screenName = barWindow.screen.name; const screenName = barWindow.screen.name;
const activePopout = PopoutManager.currentPopoutsByScreen[screenName]; const activePopout = PopoutManager.currentPopoutsByScreen[screenName];
const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName]; const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName];
@@ -22,12 +22,12 @@ DankPopout {
function setProfile(profile) { function setProfile(profile) {
if (typeof PowerProfiles === "undefined") { if (typeof PowerProfiles === "undefined") {
ToastService.showError(I18n.tr("power-profiles-daemon not available")); ToastService.showError("power-profiles-daemon not available");
return; return;
} }
PowerProfiles.profile = profile; PowerProfiles.profile = profile;
if (PowerProfiles.profile !== profile) { if (PowerProfiles.profile !== profile) {
ToastService.showError(I18n.tr("Failed to set power profile")); ToastService.showError("Failed to set power profile");
} }
} }
@@ -173,10 +173,10 @@ DankPopout {
width: parent.width - Theme.iconSizeLarge - 32 - Theme.spacingM * 2 width: parent.width - Theme.iconSizeLarge - 32 - Theme.spacingM * 2
readonly property string timeInfoText: { readonly property string timeInfoText: {
if (!BatteryService.batteryAvailable) if (!BatteryService.batteryAvailable)
return I18n.tr("Power profile management available"); return "Power profile management available";
const time = BatteryService.formatTimeRemaining(); const time = BatteryService.formatTimeRemaining();
if (time !== "Unknown") { if (time !== "Unknown") {
return BatteryService.isCharging ? I18n.tr("Time until full: %1").arg(time) : I18n.tr("Time remaining: %1").arg(time); return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`;
} }
return ""; return "";
} }
@@ -188,7 +188,7 @@ DankPopout {
spacing: Theme.spacingS spacing: Theme.spacingS
StyledText { StyledText {
text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : I18n.tr("Power") text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : "Power"
font.pixelSize: Theme.fontSizeXLarge font.pixelSize: Theme.fontSizeXLarge
color: { color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) { if (BatteryService.isLowBattery && !BatteryService.isCharging) {
@@ -338,7 +338,7 @@ DankPopout {
} }
StyledText { StyledText {
text: BatteryService.batteryCapacity > 0 ? `${BatteryService.batteryCapacity.toFixed(1)} Wh` : I18n.tr("Unknown") text: BatteryService.batteryCapacity > 0 ? `${BatteryService.batteryCapacity.toFixed(1)} Wh` : "Unknown"
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Bold font.weight: Font.Bold
@@ -393,7 +393,7 @@ DankPopout {
width: parent.width - percentText.width - chargingIcon.width - Theme.spacingM * 2 width: parent.width - percentText.width - chargingIcon.width - Theme.spacingM * 2
StyledText { StyledText {
text: modelData.model || I18n.tr("Battery %1").arg(index + 1) text: modelData.model || `Battery ${index + 1}`
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
@@ -1,4 +1,3 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Common import qs.Common
@@ -39,20 +38,12 @@ BasePill {
property var _vAudio: null property var _vAudio: null
property var _vBrightness: null property var _vBrightness: null
property var _vMic: null property var _vMic: null
property var _interactionDelegates: []
readonly property var defaultControlCenterGroupOrder: ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing"]
readonly property var effectiveControlCenterGroupOrder: getEffectiveControlCenterGroupOrder()
readonly property var controlCenterRenderModel: getControlCenterRenderModel()
onIsVerticalOrientationChanged: root.clearInteractionRefs()
onWheel: function (wheelEvent) { onWheel: function (wheelEvent) {
const delta = wheelEvent.angleDelta.y; const delta = wheelEvent.angleDelta.y;
if (delta === 0) if (delta === 0)
return; return;
root.refreshInteractionRefs();
const rootX = wheelEvent.x - root.leftMargin; const rootX = wheelEvent.x - root.leftMargin;
const rootY = wheelEvent.y - root.topMargin; const rootY = wheelEvent.y - root.topMargin;
@@ -81,8 +72,6 @@ BasePill {
} }
onRightClicked: function (rootX, rootY) { onRightClicked: function (rootX, rootY) {
root.refreshInteractionRefs();
if (root.isVerticalOrientation && _vCol) { if (root.isVerticalOrientation && _vCol) {
const pos = root.mapToItem(_vCol, rootX, rootY); const pos = root.mapToItem(_vCol, rootX, rootY);
if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) { if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) {
@@ -290,142 +279,26 @@ BasePill {
return CupsService.getTotalJobsNum() > 0; return CupsService.getTotalJobsNum() > 0;
} }
function getControlCenterIconSize() {
return Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale);
}
function getEffectiveControlCenterGroupOrder() {
const knownIds = root.defaultControlCenterGroupOrder;
const savedOrder = root.widgetData?.controlCenterGroupOrder;
const result = [];
const seen = {};
if (savedOrder && typeof savedOrder.length === "number") {
for (let i = 0; i < savedOrder.length; ++i) {
const groupId = savedOrder[i];
if (knownIds.indexOf(groupId) === -1 || seen[groupId])
continue;
seen[groupId] = true;
result.push(groupId);
}
}
for (let i = 0; i < knownIds.length; ++i) {
const groupId = knownIds[i];
if (seen[groupId])
continue;
seen[groupId] = true;
result.push(groupId);
}
return result;
}
function isGroupVisible(groupId) {
switch (groupId) {
case "screenSharing":
return root.showScreenSharingIcon && NiriService.hasCasts;
case "network":
return root.showNetworkIcon && NetworkService.networkAvailable;
case "vpn":
return root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected;
case "bluetooth":
return root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled;
case "audio":
return root.showAudioIcon;
case "microphone":
return root.showMicIcon;
case "brightness":
return root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice();
case "battery":
return root.showBatteryIcon && BatteryService.batteryAvailable;
case "printer":
return root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs();
default:
return false;
}
}
function isCompositeGroup(groupId) {
return groupId === "audio" || groupId === "microphone" || groupId === "brightness";
}
function getControlCenterRenderModel() {
return root.effectiveControlCenterGroupOrder.map(groupId => ({
"id": groupId,
"visible": root.isGroupVisible(groupId),
"composite": root.isCompositeGroup(groupId)
}));
}
function clearInteractionRefs() {
root._hAudio = null;
root._hBrightness = null;
root._hMic = null;
root._vAudio = null;
root._vBrightness = null;
root._vMic = null;
}
function registerInteractionDelegate(isVertical, item) {
if (!item)
return;
for (let i = 0; i < root._interactionDelegates.length; ++i) {
const entry = root._interactionDelegates[i];
if (entry && entry.item === item) {
entry.isVertical = isVertical;
return;
}
}
root._interactionDelegates = root._interactionDelegates.concat([
{
"isVertical": isVertical,
"item": item
}
]);
}
function unregisterInteractionDelegate(item) {
if (!item)
return;
root._interactionDelegates = root._interactionDelegates.filter(entry => entry && entry.item !== item);
}
function refreshInteractionRefs() {
root.clearInteractionRefs();
for (let i = 0; i < root._interactionDelegates.length; ++i) {
const entry = root._interactionDelegates[i];
const item = entry?.item;
if (!item || !item.visible)
continue;
const groupId = item.interactionGroupId;
if (entry.isVertical) {
if (groupId === "audio")
root._vAudio = item;
else if (groupId === "microphone")
root._vMic = item;
else if (groupId === "brightness")
root._vBrightness = item;
} else {
if (groupId === "audio")
root._hAudio = item;
else if (groupId === "microphone")
root._hMic = item;
else if (groupId === "brightness")
root._hBrightness = item;
}
}
}
function hasNoVisibleIcons() { function hasNoVisibleIcons() {
return !root.controlCenterRenderModel.some(entry => entry.visible); if (root.showScreenSharingIcon && NiriService.hasCasts)
return false;
if (root.showNetworkIcon && NetworkService.networkAvailable)
return false;
if (root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected)
return false;
if (root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled)
return false;
if (root.showAudioIcon)
return false;
if (root.showMicIcon)
return false;
if (root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice())
return false;
if (root.showBatteryIcon && BatteryService.batteryAvailable)
return false;
if (root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs())
return false;
return true;
} }
content: Component { content: Component {
@@ -436,7 +309,12 @@ BasePill {
Component.onCompleted: { Component.onCompleted: {
root._hRow = controlIndicators; root._hRow = controlIndicators;
root._vCol = controlColumn; root._vCol = controlColumn;
root.clearInteractionRefs(); root._hAudio = audioIcon.parent;
root._hBrightness = brightnessIcon.parent;
root._hMic = micIcon.parent;
root._vAudio = audioIconV.parent;
root._vBrightness = brightnessIconV.parent;
root._vMic = micIconV.parent;
} }
Column { Column {
@@ -446,151 +324,159 @@ BasePill {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXS spacing: Theme.spacingXS
Repeater { Item {
model: root.controlCenterRenderModel width: parent.width
Item { height: root.vIconSize
id: verticalGroupItem visible: root.showScreenSharingIcon && NiriService.hasCasts
required property var modelData
required property int index
property string interactionGroupId: modelData.id
width: parent.width DankIcon {
height: { name: "screen_record"
switch (modelData.id) { size: root.vIconSize
case "audio": color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
return root.vIconSize + (audioPercentV.visible ? audioPercentV.implicitHeight + 2 : 0); anchors.centerIn: parent
case "microphone": }
return root.vIconSize + (micPercentV.visible ? micPercentV.implicitHeight + 2 : 0); }
case "brightness":
return root.vIconSize + (brightnessPercentV.visible ? brightnessPercentV.implicitHeight + 2 : 0);
default:
return root.vIconSize;
}
}
visible: modelData.visible
Component.onCompleted: { Item {
root.registerInteractionDelegate(true, verticalGroupItem); width: parent.width
root.refreshInteractionRefs(); height: root.vIconSize
} visible: root.showNetworkIcon && NetworkService.networkAvailable
Component.onDestruction: {
if (root) {
root.unregisterInteractionDelegate(verticalGroupItem);
root.refreshInteractionRefs();
}
}
onVisibleChanged: root.refreshInteractionRefs()
onInteractionGroupIdChanged: {
root.refreshInteractionRefs();
}
DankIcon { DankIcon {
anchors.centerIn: parent name: root.getNetworkIconName()
visible: !verticalGroupItem.modelData.composite size: root.vIconSize
name: { color: root.getNetworkIconColor()
switch (verticalGroupItem.modelData.id) { anchors.centerIn: parent
case "screenSharing": }
return "screen_record"; }
case "network":
return root.getNetworkIconName();
case "vpn":
return "vpn_lock";
case "bluetooth":
return "bluetooth";
case "battery":
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
case "printer":
return "print";
default:
return "settings";
}
}
size: root.vIconSize
color: {
switch (verticalGroupItem.modelData.id) {
case "screenSharing":
return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText;
case "network":
return root.getNetworkIconColor();
case "vpn":
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
case "bluetooth":
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
case "battery":
return root.getBatteryIconColor();
case "printer":
return Theme.primary;
default:
return Theme.widgetIconColor;
}
}
}
DankIcon { Item {
id: audioIconV width: parent.width
visible: verticalGroupItem.modelData.id === "audio" height: root.vIconSize
name: root.getVolumeIconName() visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
size: root.vIconSize
color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
NumericText { DankIcon {
id: audioPercentV name: "vpn_lock"
visible: verticalGroupItem.modelData.id === "audio" && root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume) size: root.vIconSize
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%" color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
reserveText: "100%" anchors.centerIn: parent
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) }
color: Theme.widgetTextColor }
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: audioIconV.bottom
anchors.topMargin: 2
}
DankIcon { Item {
id: micIconV width: parent.width
visible: verticalGroupItem.modelData.id === "microphone" height: root.vIconSize
name: root.getMicIconName() visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
size: root.vIconSize
color: root.getMicIconColor()
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
NumericText { DankIcon {
id: micPercentV name: "bluetooth"
visible: verticalGroupItem.modelData.id === "microphone" && root.showMicPercent && isFinite(AudioService.source?.audio?.volume) size: root.vIconSize
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%" color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
reserveText: "100%" anchors.centerIn: parent
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) }
color: Theme.widgetTextColor }
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: micIconV.bottom
anchors.topMargin: 2
}
DankIcon { Item {
id: brightnessIconV width: parent.width
visible: verticalGroupItem.modelData.id === "brightness" height: root.vIconSize + (root.showAudioPercent ? audioPercentV.implicitHeight + 2 : 0)
name: root.getBrightnessIconName() visible: root.showAudioIcon
size: root.vIconSize
color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
NumericText { DankIcon {
id: brightnessPercentV id: audioIconV
visible: verticalGroupItem.modelData.id === "brightness" && root.showBrightnessPercent && isFinite(getBrightness()) name: root.getVolumeIconName()
text: Math.round(getBrightness() * 100) + "%" size: root.vIconSize
reserveText: "100%" color: Theme.widgetIconColor
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) anchors.horizontalCenter: parent.horizontalCenter
color: Theme.widgetTextColor anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter }
anchors.top: brightnessIconV.bottom
anchors.topMargin: 2 StyledText {
} id: audioPercentV
visible: root.showAudioPercent
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: audioIconV.bottom
anchors.topMargin: 2
}
}
Item {
width: parent.width
height: root.vIconSize + (root.showMicPercent ? micPercentV.implicitHeight + 2 : 0)
visible: root.showMicIcon
DankIcon {
id: micIconV
name: root.getMicIconName()
size: root.vIconSize
color: root.getMicIconColor()
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
StyledText {
id: micPercentV
visible: root.showMicPercent
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: micIconV.bottom
anchors.topMargin: 2
}
}
Item {
width: parent.width
height: root.vIconSize + (root.showBrightnessPercent ? brightnessPercentV.implicitHeight + 2 : 0)
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
DankIcon {
id: brightnessIconV
name: root.getBrightnessIconName()
size: root.vIconSize
color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
StyledText {
id: brightnessPercentV
visible: root.showBrightnessPercent
text: Math.round(getBrightness() * 100) + "%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: brightnessIconV.bottom
anchors.topMargin: 2
}
}
Item {
width: parent.width
height: root.vIconSize
visible: root.showBatteryIcon && BatteryService.batteryAvailable
DankIcon {
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: root.vIconSize
color: root.getBatteryIconColor()
anchors.centerIn: parent
}
}
Item {
width: parent.width
height: root.vIconSize
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
DankIcon {
name: "print"
size: root.vIconSize
color: Theme.primary
anchors.centerIn: parent
} }
} }
@@ -614,206 +500,149 @@ BasePill {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingXS spacing: Theme.spacingXS
Repeater { DankIcon {
model: root.controlCenterRenderModel name: "screen_record"
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: root.showScreenSharingIcon && NiriService.hasCasts
}
Item { DankIcon {
id: horizontalGroupItem id: networkIcon
required property var modelData name: root.getNetworkIconName()
required property int index size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
property string interactionGroupId: modelData.id color: root.getNetworkIconColor()
anchors.verticalCenter: parent.verticalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable
}
width: { DankIcon {
switch (modelData.id) { id: vpnIcon
case "audio": name: "vpn_lock"
return audioGroup.width; size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
case "microphone": color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
return micGroup.width; anchors.verticalCenter: parent.verticalCenter
case "brightness": visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
return brightnessGroup.width; }
default:
return root.getControlCenterIconSize();
}
}
implicitWidth: width
height: root.widgetThickness - root.horizontalPadding * 2
visible: modelData.visible
Component.onCompleted: { DankIcon {
root.registerInteractionDelegate(false, horizontalGroupItem); id: bluetoothIcon
root.refreshInteractionRefs(); name: "bluetooth"
} size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
Component.onDestruction: { color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
if (root) { anchors.verticalCenter: parent.verticalCenter
root.unregisterInteractionDelegate(horizontalGroupItem); visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
root.refreshInteractionRefs(); }
}
}
onVisibleChanged: root.refreshInteractionRefs()
onInteractionGroupIdChanged: {
root.refreshInteractionRefs();
}
DankIcon { Rectangle {
id: iconOnlyItem width: audioIcon.implicitWidth + (root.showAudioPercent ? audioPercent.implicitWidth : 0) + 4
anchors.verticalCenter: parent.verticalCenter height: root.widgetThickness - root.horizontalPadding * 2
anchors.left: parent.left color: "transparent"
visible: !horizontalGroupItem.modelData.composite anchors.verticalCenter: parent.verticalCenter
name: { visible: root.showAudioIcon
switch (horizontalGroupItem.modelData.id) {
case "screenSharing":
return "screen_record";
case "network":
return root.getNetworkIconName();
case "vpn":
return "vpn_lock";
case "bluetooth":
return "bluetooth";
case "battery":
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
case "printer":
return "print";
default:
return "settings";
}
}
size: root.getControlCenterIconSize()
color: {
switch (horizontalGroupItem.modelData.id) {
case "screenSharing":
return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText;
case "network":
return root.getNetworkIconColor();
case "vpn":
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
case "bluetooth":
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
case "battery":
return root.getBatteryIconColor();
case "printer":
return Theme.primary;
default:
return Theme.widgetIconColor;
}
}
}
Rectangle { DankIcon {
id: audioGroup id: audioIcon
width: audioContent.implicitWidth + 2 name: root.getVolumeIconName()
implicitWidth: width size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
height: parent.height color: Theme.widgetIconColor
color: "transparent" anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left
visible: horizontalGroupItem.modelData.id === "audio" anchors.leftMargin: 2
}
Row { StyledText {
id: audioContent id: audioPercent
anchors.left: parent.left visible: root.showAudioPercent
anchors.leftMargin: 1 text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
anchors.verticalCenter: parent.verticalCenter font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
spacing: 2 color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: audioIcon.right
anchors.leftMargin: 2
}
}
DankIcon { Rectangle {
id: audioIcon width: micIcon.implicitWidth + (root.showMicPercent ? micPercent.implicitWidth : 0) + 4
name: root.getVolumeIconName() height: root.widgetThickness - root.horizontalPadding * 2
size: root.getControlCenterIconSize() color: "transparent"
color: Theme.widgetIconColor anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenter: parent.verticalCenter visible: root.showMicIcon
}
NumericText { DankIcon {
id: audioPercent id: micIcon
visible: root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume) name: root.getMicIconName()
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%" size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
reserveText: "100%" color: root.getMicIconColor()
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) anchors.verticalCenter: parent.verticalCenter
color: Theme.widgetTextColor anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 2
width: visible ? implicitWidth : 0 }
}
}
}
Rectangle { StyledText {
id: micGroup id: micPercent
width: micContent.implicitWidth + 2 visible: root.showMicPercent
implicitWidth: width text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
height: parent.height font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: "transparent" color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: horizontalGroupItem.modelData.id === "microphone" anchors.left: micIcon.right
anchors.leftMargin: 2
}
}
Row { Rectangle {
id: micContent width: brightnessIcon.implicitWidth + (root.showBrightnessPercent ? brightnessPercent.implicitWidth : 0) + 4
anchors.left: parent.left height: root.widgetThickness - root.horizontalPadding * 2
anchors.leftMargin: 1 color: "transparent"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 2 visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
DankIcon { DankIcon {
id: micIcon id: brightnessIcon
name: root.getMicIconName() name: root.getBrightnessIconName()
size: root.getControlCenterIconSize() size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: root.getMicIconColor() color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} anchors.left: parent.left
anchors.leftMargin: 2
}
NumericText { StyledText {
id: micPercent id: brightnessPercent
visible: root.showMicPercent && isFinite(AudioService.source?.audio?.volume) visible: root.showBrightnessPercent
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%" text: Math.round(getBrightness() * 100) + "%"
reserveText: "100%" font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) color: Theme.widgetTextColor
color: Theme.widgetTextColor anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenter: parent.verticalCenter anchors.left: brightnessIcon.right
width: visible ? implicitWidth : 0 anchors.leftMargin: 2
}
}
}
Rectangle {
id: brightnessGroup
width: brightnessContent.implicitWidth + 2
implicitWidth: width
height: parent.height
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: horizontalGroupItem.modelData.id === "brightness"
Row {
id: brightnessContent
anchors.left: parent.left
anchors.leftMargin: 1
anchors.verticalCenter: parent.verticalCenter
spacing: 2
DankIcon {
id: brightnessIcon
name: root.getBrightnessIconName()
size: root.getControlCenterIconSize()
color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter
}
NumericText {
id: brightnessPercent
visible: root.showBrightnessPercent && isFinite(getBrightness())
text: Math.round(getBrightness() * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
width: visible ? implicitWidth : 0
}
}
}
} }
} }
DankIcon {
id: batteryIcon
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: root.getBatteryIconColor()
anchors.verticalCenter: parent.verticalCenter
visible: root.showBatteryIcon && BatteryService.batteryAvailable
}
DankIcon {
id: printerIcon
name: "print"
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
}
DankIcon { DankIcon {
name: "settings" name: "settings"
size: root.getControlCenterIconSize() size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: root.isActive ? Theme.primary : Theme.widgetIconColor color: root.isActive ? Theme.primary : Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.hasNoVisibleIcons() visible: root.hasNoVisibleIcons()
@@ -87,11 +87,11 @@ BasePill {
} }
const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId); const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId);
return workspaceWindows.length > 0 && activeWindow && (activeWindow.title || activeWindow.appId); return workspaceWindows.length > 0 && activeWindow && activeWindow.title;
} }
if (CompositorService.isHyprland) { if (CompositorService.isHyprland) {
if (!Hyprland.focusedWorkspace || !activeWindow || !(activeWindow.title || activeWindow.appId)) { if (!Hyprland.focusedWorkspace || !activeWindow || !activeWindow.title) {
return false; return false;
} }
@@ -111,7 +111,7 @@ BasePill {
} }
} }
return activeWindow && (activeWindow.title || activeWindow.appId); return activeWindow && activeWindow.title;
} }
width: hasWindowsOnCurrentWorkspace ? (isVerticalOrientation ? barThickness : visualWidth) : 0 width: hasWindowsOnCurrentWorkspace ? (isVerticalOrientation ? barThickness : visualWidth) : 0
@@ -211,20 +211,17 @@ BasePill {
text: { text: {
const title = activeWindow && activeWindow.title ? activeWindow.title : ""; const title = activeWindow && activeWindow.title ? activeWindow.title : "";
const appName = appText.text; const appName = appText.text;
if (!title || !appName) {
if (compactMode) {
if (!title || title === appName)
return title || appName;
if (title.endsWith(appName))
return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "") || appName;
return title; return title;
} }
if (!title || !appName) if (title.endsWith(" - " + appName)) {
return title; return title.substring(0, title.length - (" - " + appName).length);
}
if (title.endsWith(appName)) if (title.endsWith(appName)) {
return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, ""); return title.substring(0, title.length - appName.length).replace(/ - $/, "");
}
return title; return title;
} }
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
@@ -161,23 +162,6 @@ BasePill {
return 0; return 0;
} }
readonly property string autoBarShadowDirection: {
const edge = root.axis?.edge;
switch (edge) {
case "top":
return "top";
case "bottom":
return "bottom";
case "left":
return "left";
case "right":
return "right";
default:
return "bottom";
}
}
readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
property bool menuOpen: false property bool menuOpen: false
property var currentTrayMenu: null property var currentTrayMenu: null
@@ -956,6 +940,13 @@ BasePill {
} }
})(), overflowMenu.dpr) })(), overflowMenu.dpr)
readonly property var elev: Theme.elevationLevel2
property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8
property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0
property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25
readonly property real popupSurfaceAlpha: Theme.popupTransparency
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
opacity: root.menuOpen ? 1 : 0 opacity: root.menuOpen ? 1 : 0
scale: root.menuOpen ? 1 : 0.85 scale: root.menuOpen ? 1 : 0.85
@@ -976,14 +967,19 @@ BasePill {
ElevationShadow { ElevationShadow {
id: bgShadowLayer id: bgShadowLayer
anchors.fill: parent anchors.fill: parent
level: Theme.elevationLevel3 level: menuContainer.elev
direction: root.effectiveShadowDirection fallbackOffset: 4
fallbackOffset: 6 shadowBlurPx: menuContainer.shadowBlurPx
shadowSpreadPx: menuContainer.shadowSpreadPx
shadowColor: {
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
}
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
targetRadius: Theme.cornerRadius targetRadius: Theme.cornerRadius
sourceRect.antialiasing: true sourceRect.antialiasing: true
sourceRect.smooth: true sourceRect.smooth: true
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled shadowEnabled: Theme.elevationEnabled
layer.smooth: true layer.smooth: true
layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2)) layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
layer.textureMirroring: ShaderEffectSource.MirrorVertically layer.textureMirroring: ShaderEffectSource.MirrorVertically
@@ -1406,6 +1402,13 @@ BasePill {
} }
})(), menuWindow.dpr) })(), menuWindow.dpr)
readonly property var elev: Theme.elevationLevel2
property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8
property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0
property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25
readonly property real popupSurfaceAlpha: Theme.popupTransparency
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
opacity: menuRoot.showMenu ? 1 : 0 opacity: menuRoot.showMenu ? 1 : 0
scale: menuRoot.showMenu ? 1 : 0.85 scale: menuRoot.showMenu ? 1 : 0.85
@@ -1426,13 +1429,18 @@ BasePill {
ElevationShadow { ElevationShadow {
id: menuBgShadowLayer id: menuBgShadowLayer
anchors.fill: parent anchors.fill: parent
level: Theme.elevationLevel3 level: menuContainer.elev
direction: root.effectiveShadowDirection fallbackOffset: 4
fallbackOffset: 6 shadowBlurPx: menuContainer.shadowBlurPx
shadowSpreadPx: menuContainer.shadowSpreadPx
shadowColor: {
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
}
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
targetRadius: Theme.cornerRadius targetRadius: Theme.cornerRadius
sourceRect.antialiasing: true sourceRect.antialiasing: true
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled shadowEnabled: Theme.elevationEnabled
layer.smooth: true layer.smooth: true
layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr)) layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
layer.textureMirroring: ShaderEffectSource.MirrorVertically layer.textureMirroring: ShaderEffectSource.MirrorVertically
@@ -775,11 +775,6 @@ Item {
} }
onWheel: wheel => { onWheel: wheel => {
if (Math.abs(wheel.angleDelta.x) > Math.abs(wheel.angleDelta.y)) {
wheel.accepted = false;
return;
}
if (scrollInProgress) if (scrollInProgress)
return; return;
@@ -1475,10 +1470,6 @@ Item {
delegate: Item { delegate: Item {
width: root.appIconSize width: root.appIconSize
height: root.appIconSize height: root.appIconSize
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
readonly property real appOpacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
IconImage { IconImage {
id: rowAppIcon id: rowAppIcon
@@ -1494,14 +1485,14 @@ Item {
color: Theme.surfaceContainer color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40) radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1 border.width: 1
border.color: appBorderColor border.color: Theme.primarySelected
opacity: appOpacity opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: (modelData.fallbackText || "?").charAt(0).toUpperCase() text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
font.pixelSize: parent.width * 0.45 font.pixelSize: parent.width * 0.45
color: appGlyphColor color: Theme.primary
font.weight: Font.Bold font.weight: Font.Bold
} }
} }
@@ -1512,14 +1503,14 @@ Item {
color: Theme.surfaceContainer color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40) radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1 border.width: 1
border.color: appBorderColor border.color: Theme.primarySelected
opacity: appOpacity opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
size: parent.width * 0.7 size: parent.width * 0.7
name: "sports_esports" name: "sports_esports"
color: appGlyphColor color: Theme.primary
} }
} }
@@ -1532,12 +1523,11 @@ Item {
layer.effect: MultiEffect { layer.effect: MultiEffect {
saturation: 0 saturation: 0
colorization: 1 colorization: 1
colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor) colorizationColor: isActive ? quickshellIconActiveColor : quickshellIconInactiveColor
} }
} }
IconImage { IconImage {
id: rowSteamIcon
anchors.fill: parent anchors.fill: parent
source: modelData.icon source: modelData.icon
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
@@ -1548,21 +1538,11 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
size: root.appIconSize size: root.appIconSize
name: "sports_esports" name: "sports_esports"
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor color: Theme.widgetTextColor
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
visible: modelData.isSteamApp && !modelData.icon visible: modelData.isSteamApp && !modelData.icon
} }
Rectangle {
anchors.fill: parent
visible: (rowAppIcon.visible || rowSteamIcon.visible || modelData.isQuickshell) && appHighlightActive
color: "transparent"
radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1
border.color: focusedBorderColor
z: 1
}
MouseArea { MouseArea {
id: rowAppMouseArea id: rowAppMouseArea
anchors.fill: parent anchors.fill: parent
@@ -1644,10 +1624,6 @@ Item {
delegate: Item { delegate: Item {
width: root.appIconSize width: root.appIconSize
height: root.appIconSize height: root.appIconSize
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
readonly property real appOpacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
IconImage { IconImage {
id: colAppIcon id: colAppIcon
@@ -1663,14 +1639,14 @@ Item {
color: Theme.surfaceContainer color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40) radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1 border.width: 1
border.color: appBorderColor border.color: Theme.primarySelected
opacity: appOpacity opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: (modelData.fallbackText || "?").charAt(0).toUpperCase() text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
font.pixelSize: parent.width * 0.45 font.pixelSize: parent.width * 0.45
color: appGlyphColor color: Theme.primary
font.weight: Font.Bold font.weight: Font.Bold
} }
} }
@@ -1681,14 +1657,14 @@ Item {
color: Theme.surfaceContainer color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40) radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1 border.width: 1
border.color: appBorderColor border.color: Theme.primarySelected
opacity: appOpacity opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
size: parent.width * 0.7 size: parent.width * 0.7
name: "sports_esports" name: "sports_esports"
color: appGlyphColor color: Theme.primary
} }
} }
@@ -1701,12 +1677,11 @@ Item {
layer.effect: MultiEffect { layer.effect: MultiEffect {
saturation: 0 saturation: 0
colorization: 1 colorization: 1
colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor) colorizationColor: isActive ? quickshellIconActiveColor : quickshellIconInactiveColor
} }
} }
IconImage { IconImage {
id: colSteamIcon
anchors.fill: parent anchors.fill: parent
source: modelData.icon source: modelData.icon
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
@@ -1717,21 +1692,11 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
size: root.appIconSize size: root.appIconSize
name: "sports_esports" name: "sports_esports"
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor color: Theme.widgetTextColor
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
visible: modelData.isSteamApp && !modelData.icon visible: modelData.isSteamApp && !modelData.icon
} }
Rectangle {
anchors.fill: parent
visible: (colAppIcon.visible || colSteamIcon.visible || modelData.isQuickshell) && appHighlightActive
color: "transparent"
radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1
border.color: focusedBorderColor
z: 1
}
MouseArea { MouseArea {
id: colAppMouseArea id: colAppMouseArea
anchors.fill: parent anchors.fill: parent
@@ -12,11 +12,10 @@ DankPopout {
property var triggerScreen: null property var triggerScreen: null
property int currentTabIndex: 0 property int currentTabIndex: 0
popupWidth: SettingsData.showWeekNumber ? 736 : 700 popupWidth: 700
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500 popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
triggerWidth: 80 triggerWidth: 80
screen: triggerScreen screen: triggerScreen
shouldBeVisible: dashVisible
property bool __focusArmed: false property bool __focusArmed: false
property bool __contentReady: false property bool __contentReady: false
@@ -168,7 +167,6 @@ DankPopout {
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
implicitWidth: Math.max(700, pages.implicitWidth + (Theme.spacingM * 2))
implicitHeight: contentColumn.height + Theme.spacingM * 2 implicitHeight: contentColumn.height + Theme.spacingM * 2
color: "transparent" color: "transparent"
focus: true focus: true
@@ -317,7 +315,6 @@ DankPopout {
id: pages id: pages
width: parent.width width: parent.width
height: implicitHeight height: implicitHeight
implicitWidth: currentItem && currentItem.implicitWidth > 0 ? currentItem.implicitWidth : (700 - Theme.spacingM * 2)
implicitHeight: { implicitHeight: {
if (root.currentTabIndex === 0) if (root.currentTabIndex === 0)
return overviewLoader.item?.implicitHeight ?? 410; return overviewLoader.item?.implicitHeight ?? 410;
@@ -44,6 +44,43 @@ Item {
property int __volumeHoverCount: 0 property int __volumeHoverCount: 0
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
function panelMotionX(panelWidth, active) {
if (active)
return 0;
if (directionalEffect) {
const travel = Math.max(Theme.effectAnimOffset, panelWidth * 0.85);
return isRightEdge ? -travel : travel;
}
if (depthEffect) {
const travel = Math.max(Theme.effectAnimOffset * 0.7, panelWidth * 0.32);
return isRightEdge ? -travel : travel;
}
return 0;
}
function panelMotionY(panelType, panelHeight, active) {
if (active)
return 0;
if (directionalEffect) {
if (panelType === 2)
return panelHeight * 0.08;
if (panelType === 3)
return -panelHeight * 0.08;
return 0;
}
if (depthEffect) {
if (panelType === 2)
return panelHeight * 0.04;
if (panelType === 3)
return -panelHeight * 0.04;
return 0;
}
return 0;
}
function volumeAreaEntered() { function volumeAreaEntered() {
__volumeHoverCount++; __volumeHoverCount++;
panelEntered(); panelEntered();
@@ -62,30 +99,47 @@ Item {
visible: dropdownType === 1 && volumeAvailable visible: dropdownType === 1 && volumeAvailable
width: 60 width: 60
height: 180 height: 180
x: isRightEdge ? anchorPos.x : anchorPos.x - width x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 1)
y: anchorPos.y - height / 2 y: anchorPos.y - height / 2 + panelMotionY(1, height, dropdownType === 1)
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1 border.width: 1
opacity: dropdownType === 1 ? 1 : 0 opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : 0)
scale: dropdownType === 1 ? 1 : 0.96 scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : Theme.effectScaleCollapsed)
transformOrigin: isRightEdge ? Item.Left : Item.Right transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity { Behavior on opacity {
NumberAnimation { enabled: !Theme.isDirectionalEffect
duration: Theme.expressiveDurations.expressiveDefaultSpatial DankAnim {
easing.type: Easing.BezierSpline duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) * Theme.variantOpacityDurationScale)
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
Behavior on scale { Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
@@ -197,33 +251,50 @@ Item {
Rectangle { Rectangle {
id: audioDevicesPanel id: audioDevicesPanel
visible: dropdownType === 2 visible: dropdownType === 2 && activePlayer !== null
width: 280 width: 280
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100)) height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
x: isRightEdge ? anchorPos.x : anchorPos.x - width x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 2)
y: anchorPos.y - height / 2 y: anchorPos.y - height / 2 + panelMotionY(2, height, dropdownType === 2)
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2 border.width: 2
opacity: dropdownType === 2 ? 1 : 0 opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : 0)
scale: dropdownType === 2 ? 1 : 0.96 scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : Theme.effectScaleCollapsed)
transformOrigin: isRightEdge ? Item.Left : Item.Right transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity { Behavior on opacity {
NumberAnimation { enabled: !Theme.isDirectionalEffect
duration: Theme.expressiveDurations.expressiveDefaultSpatial DankAnim {
easing.type: Easing.BezierSpline duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) * Theme.variantOpacityDurationScale)
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
Behavior on scale { Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
@@ -354,30 +425,47 @@ Item {
visible: dropdownType === 3 visible: dropdownType === 3
width: 240 width: 240
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80)) height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
x: isRightEdge ? anchorPos.x : anchorPos.x - width x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 3)
y: anchorPos.y - height / 2 y: anchorPos.y - height / 2 + panelMotionY(3, height, dropdownType === 3)
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2 border.width: 2
opacity: dropdownType === 3 ? 1 : 0 opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : 0)
scale: dropdownType === 3 ? 1 : 0.96 scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : Theme.effectScaleCollapsed)
transformOrigin: isRightEdge ? Item.Left : Item.Right transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity { Behavior on opacity {
NumberAnimation { enabled: !Theme.isDirectionalEffect
duration: Theme.expressiveDurations.expressiveDefaultSpatial DankAnim {
easing.type: Easing.BezierSpline duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) * Theme.variantOpacityDurationScale)
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
Behavior on scale { Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
@@ -105,7 +105,7 @@ Item {
return Math.max(0, Math.min(1, calculatedRatio)); return Math.max(0, Math.min(1, calculatedRatio));
} }
implicitWidth: SettingsData.showWeekNumber ? 736 : 700 implicitWidth: 700
implicitHeight: playerContent.height + playerContent.anchors.topMargin * 2 implicitHeight: playerContent.height + playerContent.anchors.topMargin * 2
Connections { Connections {
@@ -320,7 +320,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
StyledText { StyledText {
text: activePlayer?.trackTitle || I18n.tr("Unknown Track") text: activePlayer?.trackTitle || "Unknown Track"
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold font.weight: Font.Bold
color: Theme.surfaceText color: Theme.surfaceText
@@ -332,7 +332,7 @@ Item {
} }
StyledText { StyledText {
text: activePlayer?.trackTitle || I18n.tr("Unknown Artist") text: activePlayer?.trackArtist || "Unknown Artist"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width width: parent.width
@@ -669,7 +669,7 @@ Item {
const screenY = popoutY + contentOffsetY + btnY; const screenY = popoutY + contentOffsetY + btnY;
showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers); showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
} }
onEntered: sharedTooltip.show(I18n.tr("Media Players"), playerSelectorButton, 0, 0, isRightEdge ? "right" : "left") onEntered: sharedTooltip.show("Media Players", playerSelectorButton, 0, 0, isRightEdge ? "right" : "left")
onExited: sharedTooltip.hide() onExited: sharedTooltip.hide()
} }
} }
@@ -788,7 +788,7 @@ Item {
const screenY = popoutY + contentOffsetY + btnY; const screenY = popoutY + contentOffsetY + btnY;
showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight); showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight);
} }
onEntered: sharedTooltip.show(I18n.tr("Output Device"), audioDevicesButton, 0, 0, isRightEdge ? "right" : "left") onEntered: sharedTooltip.show("Output Device", audioDevicesButton, 0, 0, isRightEdge ? "right" : "left")
onExited: sharedTooltip.hide() onExited: sharedTooltip.hide()
} }
} }
@@ -7,8 +7,6 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
implicitWidth: SettingsData.showWeekNumber ? 736 : 700
property bool showEventDetails: false property bool showEventDetails: false
property date selectedDate: systemClock.date property date selectedDate: systemClock.date
property var selectedDateEvents: [] property var selectedDateEvents: []
@@ -43,40 +41,6 @@ Rectangle {
return d; return d;
} }
function getWeekNumber(dateObj) {
// Set time to noon to avoid potential Daylight Saving Time related bugs
const weekStartDay = startOfWeek(dateObj);
weekStartDay.setHours(12, 0, 0, 0);
let week1Start;
if (weekStartJs() === 1) {
// ISO 8601 Standard, week start on Monday
// A week belongs to the year its Thursday falls in
// So we have to get the yearTarget from weekStartDay instead of dateObj
let yearTarget = weekStartDay;
yearTarget.setDate(yearTarget.getDate() + 3); // Monday + 3 = Thursday
// Week 1 is the week containing Jan 4th
const jan4 = new Date(yearTarget.getFullYear(), 0, 4);
week1Start = startOfWeek(jan4);
} else {
// Traditional / US Standard, week start on Sunday
// A week belongs to the year its Sunday falls in
let yearTarget = weekStartDay;
yearTarget.setDate(yearTarget.getDate() + 6); // Monday + 6 = Sunday
// Week 1 is the week containing Jan 1st
const jan1 = new Date(yearTarget.getFullYear(), 0, 1);
week1Start = startOfWeek(jan1);
}
week1Start.setHours(12, 0, 0, 0);
const diffDays = Math.round((weekStartDay.getTime() - week1Start.getTime()) / 86400000); // Number of miliseconds in a day
return Math.floor(diffDays / 7) + 1;
}
function updateSelectedDateEvents() { function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) { if (CalendarService && CalendarService.khalAvailable) {
const events = CalendarService.getEventsForDate(selectedDate); const events = CalendarService.getEventsForDate(selectedDate);
@@ -187,7 +151,6 @@ Rectangle {
elide: Text.ElideRight elide: Text.ElideRight
} }
} }
Row { Row {
width: parent.width width: parent.width
height: 28 height: 28
@@ -261,172 +224,120 @@ Rectangle {
Row { Row {
width: parent.width width: parent.width
height: parent.height - 28 - Theme.spacingS height: 18
visible: !showEventDetails visible: !showEventDetails
spacing: SettingsData.showWeekNumber ? Theme.spacingS : 0
Column { Repeater {
id: weekNumberColumn model: {
visible: SettingsData.showWeekNumber const days = [];
width: SettingsData.showWeekNumber ? 28 : 0 const qtFirst = weekStartQt();
height: parent.height for (let i = 0; i < 7; ++i) {
spacing: Theme.spacingS const qtDay = ((qtFirst - 1 + i) % 7) + 1;
days.push(I18n.locale().dayName(qtDay, Locale.ShortFormat));
Item { }
width: parent.width return days;
height: 18
} }
Grid { Rectangle {
width: parent.width width: parent.width / 7
height: parent.height - 18 - Theme.spacingS height: 18
columns: 1 color: "transparent"
rows: 6
Repeater { StyledText {
model: 6 anchors.centerIn: parent
Rectangle { text: modelData
width: parent.width font.pixelSize: Theme.fontSizeSmall
height: parent.height / 6 color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
color: "transparent" font.weight: Font.Medium
StyledText {
anchors.centerIn: parent
text: {
const rowDate = new Date(calendarGrid.firstDay);
rowDate.setDate(rowDate.getDate() + index * 7);
return root.getWeekNumber(rowDate);
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
}
} }
} }
} }
}
Column { Grid {
width: SettingsData.showWeekNumber ? (parent.width - weekNumberColumn.width - parent.spacing) : parent.width id: calendarGrid
height: parent.height visible: !showEventDetails
spacing: Theme.spacingS
Row { property date displayDate: systemClock.date
width: parent.width property date selectedDate: systemClock.date
height: 18
Repeater { readonly property date firstDay: {
model: { const firstOfMonth = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
const days = []; return startOfWeek(firstOfMonth);
const qtFirst = weekStartQt(); }
for (let i = 0; i < 7; ++i) {
const qtDay = ((qtFirst - 1 + i) % 7) + 1; width: parent.width
days.push(I18n.locale().dayName(qtDay, Locale.ShortFormat)); height: parent.height - 28 - 18 - Theme.spacingS * 2
} columns: 7
return days; rows: 6
Repeater {
model: 42
Rectangle {
readonly property date dayDate: {
const date = new Date(parent.firstDay);
date.setDate(date.getDate() + index);
return date;
}
readonly property bool isCurrentMonth: dayDate.getMonth() === calendarGrid.displayDate.getMonth()
readonly property bool isToday: dayDate.toDateString() === new Date().toDateString()
readonly property bool isSelected: dayDate.toDateString() === calendarGrid.selectedDate.toDateString()
width: parent.width / 7
height: parent.height / 6
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - 4, parent.height - 4, 32)
height: width
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius
StyledText {
anchors.centerIn: parent
text: dayDate.getDate()
font.pixelSize: Theme.fontSizeSmall
color: isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
font.weight: isToday ? Font.Medium : Font.Normal
} }
Rectangle { Rectangle {
width: parent.width / 7 anchors.bottom: parent.bottom
height: 18 anchors.horizontalCenter: parent.horizontalCenter
color: "transparent" anchors.bottomMargin: 4
width: 12
height: 2
radius: Theme.cornerRadius
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
color: isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary
opacity: isToday ? 0.9 : 0.7
StyledText { Behavior on opacity {
anchors.centerIn: parent NumberAnimation {
text: modelData duration: Theme.shortDuration
font.pixelSize: Theme.fontSizeSmall easing.type: Theme.standardEasing
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) }
font.weight: Font.Medium
} }
} }
} }
}
Grid { MouseArea {
id: calendarGrid id: dayArea
width: parent.width anchors.fill: parent
height: parent.height - 18 - Theme.spacingS hoverEnabled: true
columns: 7 cursorShape: Qt.PointingHandCursor
rows: 6 onClicked: {
if (CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)) {
property date displayDate: systemClock.date root.selectedDate = dayDate;
property date selectedDate: systemClock.date root.showEventDetails = true;
readonly property date firstDay: {
const firstOfMonth = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
return startOfWeek(firstOfMonth);
}
Repeater {
model: 42
Rectangle {
readonly property date dayDate: {
const date = new Date(parent.firstDay);
date.setDate(date.getDate() + index);
return date;
}
readonly property bool isCurrentMonth: dayDate.getMonth() === calendarGrid.displayDate.getMonth()
readonly property bool isToday: dayDate.toDateString() === new Date().toDateString()
readonly property bool isSelected: dayDate.toDateString() === calendarGrid.selectedDate.toDateString()
width: parent.width / 7
height: parent.height / 6
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - 4, parent.height - 4, 32)
height: width
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius
StyledText {
anchors.centerIn: parent
text: dayDate.getDate()
font.pixelSize: Theme.fontSizeSmall
color: isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
font.weight: isToday ? Font.Medium : Font.Normal
}
Rectangle {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 4
width: 12
height: 2
radius: Theme.cornerRadius
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
color: isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary
opacity: isToday ? 0.9 : 0.7
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)) {
root.selectedDate = dayDate;
root.showEventDetails = true;
}
}
} }
} }
} }
} }
} }
} }
DankListView { DankListView {
width: parent.width - Theme.spacingS * 2 width: parent.width - Theme.spacingS * 2
height: parent.height - (showEventDetails ? 40 : 28 + 18) - Theme.spacingS height: parent.height - (showEventDetails ? 40 : 28 + 18) - Theme.spacingS

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