1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-13 07:42: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
160 changed files with 8253 additions and 32727 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 -1
View File
@@ -60,7 +60,7 @@ func init() {
} }
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 {
+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)
+16 -66
View File
@@ -92,25 +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 containsString(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
} }
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
@@ -210,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)
} }
@@ -395,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)
@@ -506,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 -163
View File
@@ -29,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{
@@ -201,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 {
@@ -271,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)
} }
@@ -305,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)
} }
} }
@@ -320,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)
} }
} }
@@ -410,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 containsString(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 {
@@ -515,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
-1
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 {
+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)
} }
+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
@@ -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.
+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 -8
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)
@@ -580,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();
@@ -611,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();
@@ -642,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();
@@ -673,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();
@@ -1190,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 {
@@ -1217,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 -96
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,
@@ -150,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
@@ -167,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
@@ -281,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"
@@ -315,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"
@@ -454,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
@@ -474,25 +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
@@ -538,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
@@ -577,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
@@ -1041,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();
} }
} }
@@ -1201,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) {
@@ -1293,47 +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) {
@@ -2769,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) {
@@ -2778,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();
} }
} }
} }
+21 -5
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";
} }
@@ -1551,7 +1569,7 @@ Singleton {
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");
@@ -1595,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(","));
@@ -1989,7 +2005,7 @@ 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;
} }
+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();
} }
} }
+4 -43
View File
@@ -33,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 },
@@ -48,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 },
@@ -124,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" },
@@ -166,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" },
@@ -269,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 },
@@ -290,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 },
@@ -354,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 },
@@ -393,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 {
+1 -6
View File
@@ -7,7 +7,6 @@ import qs.Modals.Clipboard
import qs.Modals.Greeter import qs.Modals.Greeter
import qs.Modals.Settings import qs.Modals.Settings
import qs.Modals.DankLauncherV2 import qs.Modals.DankLauncherV2
import qs.Modals
import qs.Modules import qs.Modules
import qs.Modules.AppDrawer import qs.Modules.AppDrawer
import qs.Modules.DankDash import qs.Modules.DankDash
@@ -314,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
@@ -620,10 +619,6 @@ Item {
} }
} }
MuxModal {
id: muxModal
}
ClipboardHistoryModal { ClipboardHistoryModal {
id: clipboardHistoryModalPopup id: clipboardHistoryModalPopup
+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
}
}
}
}
}
@@ -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();
}); });
} }
@@ -14,6 +14,7 @@ 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
@@ -23,8 +24,14 @@ Item {
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
@@ -78,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) {
@@ -97,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;
@@ -130,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() {
@@ -171,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();
} }
@@ -186,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) {
@@ -221,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();
@@ -242,7 +290,7 @@ Item {
HyprlandFocusGrab { HyprlandFocusGrab {
id: focusGrab id: focusGrab
windows: [launcherWindow] windows: [contentWindow]
active: false active: false
onCleared: { onCleared: {
@@ -267,7 +315,7 @@ Item {
if (Quickshell.screens.length === 0) if (Quickshell.screens.length === 0)
return; return;
const screen = launcherWindow.screen; const screen = contentWindow.screen;
const screenName = screen?.name; const screenName = screen?.name;
let needsReset = !screen || !screenName; let needsReset = !screen || !screenName;
@@ -289,35 +337,31 @@ Item {
return; return;
root._windowEnabled = false; root._windowEnabled = false;
launcherWindow.screen = newScreen; backgroundWindow.screen = newScreen;
contentWindow.screen = newScreen;
Qt.callLater(() => { Qt.callLater(() => {
root._windowEnabled = true; root._windowEnabled = true;
}); });
} }
} }
// Background window: fullscreen, handles darkening + click-to-dismiss
PanelWindow { PanelWindow {
id: launcherWindow id: backgroundWindow
visible: root._windowEnabled && (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
@@ -327,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
} }
@@ -339,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
} }
} }
} }
@@ -353,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
} }
@@ -86,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: {
@@ -158,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;
@@ -169,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();
@@ -202,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();
@@ -224,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:
@@ -418,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
@@ -492,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
@@ -578,7 +548,6 @@ FocusScope {
} }
} }
} }
} }
Item { Item {
@@ -727,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
@@ -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)
-621
View File
@@ -1,621 +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: I18n.tr("%1 active, %2 filtered").arg(MuxService.sessions.length).arg(muxModal.filteredSessions.length)
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(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
}
}
}
}
}
}
}
+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"),
@@ -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();
@@ -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];
@@ -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
@@ -1470,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
@@ -1489,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
} }
} }
@@ -1507,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
} }
} }
@@ -1527,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
@@ -1543,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
@@ -1639,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
@@ -1658,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
} }
} }
@@ -1676,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
} }
} }
@@ -1696,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
@@ -1712,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 {
@@ -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
+1 -1
View File
@@ -8,7 +8,7 @@ Item {
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
implicitWidth: SettingsData.showWeekNumber ? 736 : 700 implicitWidth: 700
implicitHeight: 410 implicitHeight: 410
signal switchToWeatherTab signal switchToWeatherTab
+1 -1
View File
@@ -12,7 +12,7 @@ Item {
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
implicitWidth: SettingsData.showWeekNumber ? 736 : 700 implicitWidth: 700
implicitHeight: 410 implicitHeight: 410
property string wallpaperDir: "" property string wallpaperDir: ""
+1 -1
View File
@@ -11,7 +11,7 @@ Item {
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
implicitWidth: SettingsData.showWeekNumber ? 736 : 700 implicitWidth: 700
implicitHeight: 410 implicitHeight: 410
property bool syncing: false property bool syncing: false
property bool showHourly: false property bool showHourly: false
+2 -43
View File
@@ -1,7 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets import Quickshell.Widgets
import qs.Common import qs.Common
@@ -134,40 +133,6 @@ Item {
function getGroupedToplevels() { function getGroupedToplevels() {
return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || []; return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || [];
} }
function getHyprToplevelForWayland(waylandToplevel) {
if (!waylandToplevel || !CompositorService.isHyprland || !Hyprland.toplevels)
return null;
const hyprToplevels = Array.from(Hyprland.toplevels.values);
for (let i = 0; i < hyprToplevels.length; i++) {
if (hyprToplevels[i].wayland === waylandToplevel)
return hyprToplevels[i];
}
return null;
}
function getSpecialWorkspaceName(waylandToplevel) {
const hyprToplevel = getHyprToplevelForWayland(waylandToplevel);
if (!hyprToplevel)
return "";
const wsName = String(hyprToplevel.lastIpcObject?.workspace?.name || hyprToplevel.workspace?.name || "");
if (!wsName.startsWith("special:"))
return "";
return wsName.slice("special:".length);
}
function restoreSpecialWorkspaceWindow(waylandToplevel) {
if (!SettingsData.dockRestoreSpecialWorkspaceOnClick || !CompositorService.isHyprland || !waylandToplevel)
return false;
const specialName = getSpecialWorkspaceName(waylandToplevel);
if (!specialName)
return false;
Hyprland.dispatch("togglespecialworkspace " + specialName);
Qt.callLater(() => waylandToplevel.activate());
return true;
}
onIsHoveredChanged: { onIsHoveredChanged: {
if (mouseArea.pressed || dragging) if (mouseArea.pressed || dragging)
return; return;
@@ -311,11 +276,8 @@ Item {
break; break;
case "window": case "window":
const windowToplevel = getToplevelObject(); const windowToplevel = getToplevelObject();
if (windowToplevel) { if (windowToplevel)
if (restoreSpecialWorkspaceWindow(windowToplevel))
return;
windowToplevel.activate(); windowToplevel.activate();
}
break; break;
case "grouped": case "grouped":
if (appData.windowCount === 0) { if (appData.windowCount === 0) {
@@ -338,11 +300,8 @@ Item {
SessionService.launchDesktopEntry(groupedEntry); SessionService.launchDesktopEntry(groupedEntry);
} else if (appData.windowCount === 1) { } else if (appData.windowCount === 1) {
const groupedToplevel = getToplevelObject(); const groupedToplevel = getToplevelObject();
if (groupedToplevel) { if (groupedToplevel)
if (restoreSpecialWorkspaceWindow(groupedToplevel))
return;
groupedToplevel.activate(); groupedToplevel.activate();
}
} else if (contextMenu) { } else if (contextMenu) {
const shouldHidePin = appData.appId === "org.quickshell"; const shouldHidePin = appData.appId === "org.quickshell";
contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps); contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps);
-20
View File
@@ -1,20 +0,0 @@
.pragma library
function readBoolOverride(envReader, names, fallbackValue) {
for (let i = 0; i < names.length; i++) {
const name = names[i];
const raw = envReader(name);
if (raw === undefined || raw === null || raw === "")
continue;
const normalized = String(raw).trim().toLowerCase();
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on")
return true;
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off")
return false;
console.warn("Invalid boolean override for", name + ":", raw, "- trying next override/fallback");
}
return fallbackValue;
}
+8 -29
View File
@@ -4,16 +4,13 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import "GreetdEnv.js" as GreetdEnv
Singleton { Singleton {
id: root id: root
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter" readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
readonly property string sessionConfigPath: greetCfgDir + "/session.json" readonly property string sessionConfigPath: greetCfgDir + "/session.json"
readonly property string memoryFile: greetCfgDir + "/.local/state/memory.json" readonly property string memoryFile: greetCfgDir + "/memory.json"
readonly property bool rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true)
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
property string lastSessionId: "" property string lastSessionId: ""
property string lastSuccessfulUser: "" property string lastSuccessfulUser: ""
@@ -52,44 +49,26 @@ Singleton {
if (!content || !content.trim()) if (!content || !content.trim())
return; return;
const memory = JSON.parse(content); const memory = JSON.parse(content);
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : ""; lastSessionId = memory.lastSessionId || "";
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : ""; lastSuccessfulUser = memory.lastSuccessfulUser || "";
if (!rememberLastSession || !rememberLastUser)
saveMemory();
} catch (e) { } catch (e) {
console.warn("Failed to parse greetd memory:", e); console.warn("Failed to parse greetd memory:", e);
} }
} }
function saveMemory() { function saveMemory() {
let memory = {}; memoryFileView.setText(JSON.stringify({
if (rememberLastSession && lastSessionId) "lastSessionId": lastSessionId,
memory.lastSessionId = lastSessionId; "lastSuccessfulUser": lastSuccessfulUser
if (rememberLastUser && lastSuccessfulUser) }, null, 2));
memory.lastSuccessfulUser = lastSuccessfulUser;
memoryFileView.setText(JSON.stringify(memory, null, 2));
} }
function setLastSessionId(id) { function setLastSessionId(id) {
if (!rememberLastSession) {
if (lastSessionId !== "") {
lastSessionId = "";
saveMemory();
}
return;
}
lastSessionId = id || ""; lastSessionId = id || "";
saveMemory(); saveMemory();
} }
function setLastSuccessfulUser(username) { function setLastSuccessfulUser(username) {
if (!rememberLastUser) {
if (lastSuccessfulUser !== "") {
lastSuccessfulUser = "";
saveMemory();
}
return;
}
lastSuccessfulUser = username || ""; lastSuccessfulUser = username || "";
saveMemory(); saveMemory();
} }
+48 -107
View File
@@ -5,22 +5,15 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import "GreetdEnv.js" as GreetdEnv
Singleton { Singleton {
id: root id: root
readonly property string configPath: { readonly property string configPath: {
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 + "/settings.json"; return greetCfgDir + "/settings.json";
} }
readonly property string _greeterCacheDir: {
const i = root.configPath.lastIndexOf("/");
return i >= 0 ? root.configPath.substring(0, i) : "";
}
readonly property string greeterWallpaperOverridePath: root._greeterCacheDir ? (root._greeterCacheDir + "/greeter_wallpaper_override.jpg") : ""
property string currentThemeName: "purple" property string currentThemeName: "purple"
property bool settingsLoaded: false property bool settingsLoaded: false
property string customThemeFile: "" property string customThemeFile: ""
@@ -28,12 +21,6 @@ Singleton {
property bool use24HourClock: true property bool use24HourClock: true
property bool showSeconds: false property bool showSeconds: false
property bool padHours12Hour: false property bool padHours12Hour: false
property bool greeterUse24HourClock: true
property bool greeterShowSeconds: false
property bool greeterPadHours12Hour: false
property string greeterLockDateFormat: ""
property string greeterFontFamily: ""
property string greeterWallpaperFillMode: ""
property bool useFahrenheit: false property bool useFahrenheit: false
property bool nightModeEnabled: false property bool nightModeEnabled: false
property string weatherLocation: "New York, NY" property string weatherLocation: "New York, NY"
@@ -54,11 +41,6 @@ Singleton {
property string lockDateFormat: "" property string lockDateFormat: ""
property bool lockScreenShowPowerActions: true property bool lockScreenShowPowerActions: true
property bool lockScreenShowProfileImage: true property bool lockScreenShowProfileImage: true
property bool rememberLastSession: true
property bool rememberLastUser: true
property bool greeterEnableFprint: false
property bool greeterEnableU2f: false
property string greeterWallpaperPath: ""
property bool powerActionConfirm: true property bool powerActionConfirm: true
property real powerActionHoldDuration: 0.5 property real powerActionHoldDuration: 0.5
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
@@ -70,103 +52,66 @@ Singleton {
function parseSettings(content) { function parseSettings(content) {
try { try {
let settings = {};
if (content && content.trim()) { if (content && content.trim()) {
settings = JSON.parse(content); const settings = JSON.parse(content);
} currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false;
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true;
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default";
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false;
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "";
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5;
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1;
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : Theme.defaultFontFamily;
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : Theme.defaultMonoFontFamily;
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0;
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12;
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
settingsLoaded = true;
const envRememberLastSession = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], undefined); if (typeof Theme !== "undefined") {
const envRememberLastUser = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], undefined); if (currentThemeName === "custom" && customThemeFile) {
Theme.loadCustomThemeFromFile(customThemeFile);
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple"; }
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : ""; Theme.applyGreeterTheme(currentThemeName);
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
greeterUse24HourClock = settings.greeterUse24HourClock !== undefined ? settings.greeterUse24HourClock : use24HourClock;
greeterShowSeconds = settings.greeterShowSeconds !== undefined ? settings.greeterShowSeconds : showSeconds;
greeterPadHours12Hour = settings.greeterPadHours12Hour !== undefined ? settings.greeterPadHours12Hour : padHours12Hour;
greeterLockDateFormat = settings.greeterLockDateFormat !== undefined ? settings.greeterLockDateFormat : "";
greeterFontFamily = settings.greeterFontFamily !== undefined ? settings.greeterFontFamily : "";
greeterWallpaperFillMode = settings.greeterWallpaperFillMode !== undefined ? settings.greeterWallpaperFillMode : "";
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false;
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true;
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default";
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false;
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "";
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5;
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1;
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : Theme.defaultFontFamily;
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : Theme.defaultMonoFontFamily;
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0;
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12;
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
if (envRememberLastSession !== undefined) {
rememberLastSession = envRememberLastSession;
} else {
rememberLastSession = settings.greeterRememberLastSession !== undefined ? settings.greeterRememberLastSession : settings.rememberLastSession !== undefined ? settings.rememberLastSession : true;
}
if (envRememberLastUser !== undefined) {
rememberLastUser = envRememberLastUser;
} else {
rememberLastUser = settings.greeterRememberLastUser !== undefined ? settings.greeterRememberLastUser : settings.rememberLastUser !== undefined ? settings.rememberLastUser : true;
}
greeterEnableFprint = settings.greeterEnableFprint !== undefined ? settings.greeterEnableFprint : false;
greeterEnableU2f = settings.greeterEnableU2f !== undefined ? settings.greeterEnableU2f : false;
greeterWallpaperPath = settings.greeterWallpaperPath !== undefined ? settings.greeterWallpaperPath : "";
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
if (typeof Theme !== "undefined") {
if (currentThemeName === "custom" && customThemeFile) {
Theme.loadCustomThemeFromFile(customThemeFile);
} }
Theme.applyGreeterTheme(currentThemeName);
} }
} catch (e) { } catch (e) {
console.warn("Failed to parse greetd settings:", e); console.warn("Failed to parse greetd settings:", e);
} finally {
settingsLoaded = true;
} }
} }
function getEffectiveTimeFormat() { function getEffectiveTimeFormat() {
const use24 = greeterUse24HourClock; if (use24HourClock)
const secs = greeterShowSeconds; return showSeconds ? "hh:mm:ss" : "hh:mm";
const pad = greeterPadHours12Hour; if (padHours12Hour)
if (use24) return showSeconds ? "hh:mm:ss AP" : "hh:mm AP";
return secs ? "hh:mm:ss" : "hh:mm"; return showSeconds ? "h:mm:ss AP" : "h:mm AP";
if (pad)
return secs ? "hh:mm:ss AP" : "hh:mm AP";
return secs ? "h:mm:ss AP" : "h:mm AP";
} }
function getEffectiveLockDateFormat() { function getEffectiveLockDateFormat() {
const fmt = (greeterLockDateFormat !== undefined && greeterLockDateFormat !== "") ? greeterLockDateFormat : lockDateFormat; return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat;
return fmt && fmt.length > 0 ? fmt : Locale.LongFormat;
}
function getEffectiveWallpaperFillMode() {
return (greeterWallpaperFillMode && greeterWallpaperFillMode !== "") ? greeterWallpaperFillMode : wallpaperFillMode;
}
function getEffectiveFontFamily() {
return (greeterFontFamily && greeterFontFamily !== "") ? greeterFontFamily : fontFamily;
} }
function getFilteredScreens(componentId) { function getFilteredScreens(componentId) {
@@ -188,9 +133,5 @@ Singleton {
onLoaded: { onLoaded: {
parseSettings(settingsFile.text()); parseSettings(settingsFile.text());
} }
onLoadFailed: error => {
console.warn("Failed to load greetd settings:", error);
root.parseSettings("");
}
} }
} }
+49 -711
View File
@@ -31,38 +31,6 @@ Item {
signal launchRequested signal launchRequested
property bool weatherInitialized: false property bool weatherInitialized: false
property bool awaitingExternalAuth: false
property bool pendingPasswordResponse: false
property bool passwordSubmitRequested: false
property bool cancelingExternalAuthForPassword: false
property int defaultAuthTimeoutMs: 10000
property int externalAuthTimeoutMs: 30000
property int memoryFlushDelayMs: 120
property string pendingLaunchCommand: ""
property var pendingLaunchEnv: []
property int passwordFailureCount: 0
property int passwordAttemptLimitHint: 0
property string authFeedbackMessage: ""
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 faillockConfigText: ""
property bool greeterWallpaperOverrideExists: false
property string externalAuthAutoStartedForUser: ""
property int passwordSessionTransitionRetryCount: 0
property int maxPasswordSessionTransitionRetries: 2
property bool fprintdProbeComplete: false
property bool fprintdHasDevice: false
// Falls back to PAM-only detection until the fprintd D-Bus probe completes.
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd") && (!fprintdProbeComplete || fprintdHasDevice)
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f)
readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f
function initWeatherService() { function initWeatherService() {
if (weatherInitialized) if (weatherInitialized)
@@ -76,492 +44,34 @@ Item {
WeatherService.forceRefresh(); WeatherService.forceRefresh();
} }
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 usesPamLockoutPolicy(pamText) {
if (!pamText)
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("pam_faillock.so") || line.includes("pam_tally2.so") || line.includes("pam_tally.so"))
return true;
}
return false;
}
function parsePamLineDenyValue(pamText) {
if (!pamText)
return -1;
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("pam_faillock.so") && !line.includes("pam_tally2.so") && !line.includes("pam_tally.so"))
continue;
const denyMatch = line.match(/\bdeny\s*=\s*(\d+)\b/i);
if (!denyMatch)
continue;
const parsed = parseInt(denyMatch[1], 10);
if (!isNaN(parsed))
return parsed;
}
return -1;
}
function parseFaillockDenyValue(configText) {
if (!configText)
return -1;
const lines = configText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
const denyMatch = line.match(/^deny\s*=\s*(\d+)\s*$/i);
if (!denyMatch)
continue;
const parsed = parseInt(denyMatch[1], 10);
if (!isNaN(parsed))
return parsed;
}
return -1;
}
function refreshPasswordAttemptPolicyHint() {
const pamSources = [greetdPamText, systemAuthPamText, commonAuthPamText, passwordAuthPamText, systemLoginPamText, systemLocalLoginPamText, commonAuthPcPamText, loginPamText];
let lockoutConfigured = false;
let denyFromPam = -1;
for (let i = 0; i < pamSources.length; i++) {
const source = pamSources[i];
if (!source)
continue;
if (usesPamLockoutPolicy(source))
lockoutConfigured = true;
const denyValue = parsePamLineDenyValue(source);
if (denyValue >= 0 && (denyFromPam < 0 || denyValue < denyFromPam))
denyFromPam = denyValue;
}
if (!lockoutConfigured) {
passwordAttemptLimitHint = 0;
return;
}
const denyFromConfig = parseFaillockDenyValue(faillockConfigText);
if (denyFromConfig >= 0) {
passwordAttemptLimitHint = denyFromConfig;
return;
}
if (denyFromPam >= 0) {
passwordAttemptLimitHint = denyFromPam;
return;
}
// pam_faillock default deny value when no explicit config is set.
passwordAttemptLimitHint = 3;
}
function isLikelyLockoutMessage(message) {
const lower = (message || "").toLowerCase();
return lower.includes("account is locked") || lower.includes("too many") || lower.includes("maximum number of") || lower.includes("auth_err");
}
function currentAuthMessage() {
if (GreeterState.pamState === "error")
return "Authentication error - try again";
if (GreeterState.pamState === "max")
return "Too many failed attempts - account may be locked";
if (GreeterState.pamState === "fail") {
if (passwordAttemptLimitHint > 0) {
const attempt = Math.max(1, Math.min(passwordFailureCount, passwordAttemptLimitHint));
const remaining = Math.max(passwordAttemptLimitHint - attempt, 0);
if (remaining > 0) {
return "Incorrect password - attempt " + attempt + " of " + passwordAttemptLimitHint + " (lockout may follow)";
}
return "Incorrect password - next failures may trigger account lockout";
}
return "Incorrect password";
}
return "";
}
function clearAuthFeedback() {
GreeterState.pamState = "";
authFeedbackMessage = "";
}
function resetPasswordSessionTransition(clearSubmitRequest) {
cancelingExternalAuthForPassword = false;
passwordSessionTransitionRetryCount = 0;
if (clearSubmitRequest)
passwordSubmitRequested = false;
}
Connections { Connections {
target: GreetdSettings target: GreetdSettings
function onSettingsLoadedChanged() { function onSettingsLoadedChanged() {
if (GreetdSettings.settingsLoaded) { if (GreetdSettings.settingsLoaded)
initWeatherService(); initWeatherService();
if (isPrimaryScreen) {
applyLastSuccessfulUser();
finalizeSessionSelection();
}
}
}
function onRememberLastUserChanged() {
if (!isPrimaryScreen)
return;
if (!GreetdSettings.rememberLastUser && GreetdMemory.lastSuccessfulUser) {
GreetdMemory.setLastSuccessfulUser("");
}
applyLastSuccessfulUser();
}
function onRememberLastSessionChanged() {
if (!isPrimaryScreen)
return;
if (!GreetdSettings.rememberLastSession && GreetdMemory.lastSessionId) {
GreetdMemory.setLastSessionId("");
}
finalizeSessionSelection();
}
}
FileView {
id: greetdPamWatcher
path: "/etc/pam.d/greetd"
printErrors: false
onLoaded: {
root.greetdPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.greetdPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: systemAuthPamWatcher
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: {
root.systemAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.systemAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: commonAuthPamWatcher
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: {
root.commonAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.commonAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: passwordAuthPamWatcher
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: {
root.passwordAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.passwordAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: systemLoginPamWatcher
path: "/etc/pam.d/system-login"
printErrors: false
onLoaded: {
root.systemLoginPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.systemLoginPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: systemLocalLoginPamWatcher
path: "/etc/pam.d/system-local-login"
printErrors: false
onLoaded: {
root.systemLocalLoginPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.systemLocalLoginPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: commonAuthPcPamWatcher
path: "/etc/pam.d/common-auth-pc"
printErrors: false
onLoaded: {
root.commonAuthPcPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.commonAuthPcPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: loginPamWatcher
path: "/etc/pam.d/login"
printErrors: false
onLoaded: {
root.loginPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.loginPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: faillockConfigWatcher
path: "/etc/security/faillock.conf"
printErrors: false
onLoaded: {
root.faillockConfigText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.faillockConfigText = "";
root.refreshPasswordAttemptPolicyHint();
} }
} }
Component.onCompleted: { Component.onCompleted: {
initWeatherService(); initWeatherService();
refreshPasswordAttemptPolicyHint();
if (isPrimaryScreen) if (isPrimaryScreen)
applyLastSuccessfulUser(); applyLastSuccessfulUser();
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
updateHyprlandLayout(); updateHyprlandLayout();
fprintdDeviceProbe.running = true;
} }
function applyLastSuccessfulUser() { function applyLastSuccessfulUser() {
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
return;
const lastUser = GreetdMemory.lastSuccessfulUser; const lastUser = GreetdMemory.lastSuccessfulUser;
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) { if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
GreeterState.username = lastUser; GreeterState.username = lastUser;
GreeterState.usernameInput = lastUser; GreeterState.usernameInput = lastUser;
GreeterState.showPasswordInput = true; GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(lastUser); PortalService.getGreeterUserProfileImage(lastUser);
maybeAutoStartExternalAuth();
} }
} }
function submitUsername(rawValue) {
const user = (rawValue || "").trim();
if (!user)
return;
if (GreeterState.username !== user) {
passwordFailureCount = 0;
clearAuthFeedback();
externalAuthAutoStartedForUser = "";
}
GreeterState.username = user;
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(user);
GreeterState.passwordBuffer = "";
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
maybeAutoStartExternalAuth();
}
function submitBufferedPassword() {
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
// Some PAM stacks expect an explicit empty response to advance U2F/fprint or fail normally.
Greetd.respond(GreeterState.passwordBuffer || "");
GreeterState.passwordBuffer = "";
inputField.text = "";
return true;
}
function requestPasswordSessionTransition() {
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (!passwordSubmitRequested && !hasPasswordBuffer)
return;
if (cancelingExternalAuthForPassword)
return;
if (passwordSessionTransitionRetryCount >= maxPasswordSessionTransitionRetries) {
pendingPasswordResponse = false;
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
resetPasswordSessionTransition(true);
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart();
Greetd.cancelSession();
return;
}
cancelingExternalAuthForPassword = true;
passwordSessionTransitionRetryCount = passwordSessionTransitionRetryCount + 1;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
Greetd.cancelSession();
}
function startAuthSession(submitPassword) {
submitPassword = submitPassword === true;
if (!GreeterState.showPasswordInput || !GreeterState.username)
return;
if (GreeterState.unlocking)
return;
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (Greetd.state !== GreetdState.Inactive) {
if (pendingPasswordResponse && submitPassword)
submitBufferedPassword();
else if (submitPassword)
passwordSubmitRequested = true;
return;
}
if (cancelingExternalAuthForPassword) {
if (submitPassword)
passwordSubmitRequested = true;
return;
}
if (!submitPassword && !hasPasswordBuffer && !root.greeterExternalAuthAvailable)
return;
pendingPasswordResponse = false;
passwordSubmitRequested = submitPassword;
awaitingExternalAuth = !submitPassword && !hasPasswordBuffer && root.greeterExternalAuthAvailable;
// Use greeterExternalAuthAvailable so systems with pam_fprintd but no hardware don't incur the 30 s wait.
const waitingOnPamExternalBeforePassword = submitPassword && root.greeterExternalAuthAvailable;
authTimeout.interval = (awaitingExternalAuth || waitingOnPamExternalBeforePassword) ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
authTimeout.restart();
Greetd.createSession(GreeterState.username);
}
function maybeAutoStartExternalAuth() {
if (!GreeterState.showPasswordInput || !GreeterState.username)
return;
if (!root.greeterExternalAuthAvailable)
return;
if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive)
return;
if (passwordSubmitRequested || cancelingExternalAuthForPassword)
return;
if (GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0)
return;
if (externalAuthAutoStartedForUser === GreeterState.username)
return;
externalAuthAutoStartedForUser = GreeterState.username;
startAuthSession(false);
}
function isExternalAuthPrompt(message, responseRequired) {
// Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch).
return !responseRequired;
}
Component.onDestruction: { Component.onDestruction: {
if (weatherInitialized) if (weatherInitialized)
WeatherService.removeRef(); WeatherService.removeRef();
@@ -603,34 +113,6 @@ Item {
} }
} }
// Probe fprintd D-Bus for physically enrolled scanners to eliminate PAM stack false-positives.
Process {
id: fprintdDeviceProbe
running: false
// sh wrapper: emits PROBE_UNAVAILABLE if gdbus is absent or fprintd unreachable,
// keeping the PAM-only fallback active in those cases.
command: ["sh", "-c",
"command -v gdbus >/dev/null 2>&1 || { echo PROBE_UNAVAILABLE; exit 0; }; " +
"gdbus call --system " +
"--dest net.reactivated.Fprint " +
"--object-path /net/reactivated/Fprint/Manager " +
"--method net.reactivated.Fprint.Manager.GetDevices 2>/dev/null " +
"|| echo PROBE_UNAVAILABLE"]
stdout: StdioCollector {
onStreamFinished: {
if (text.includes("PROBE_UNAVAILABLE"))
return; // PAM-only fallback stays active
root.fprintdHasDevice = text.includes("objectpath");
root.fprintdProbeComplete = true;
root.maybeAutoStartExternalAuth();
}
}
onExited: function(exitCode, exitStatus) {
if (!root.fprintdProbeComplete)
root.maybeAutoStartExternalAuth(); // PAM-only fallback stays active
}
}
Connections { Connections {
target: CompositorService.isHyprland ? Hyprland : null target: CompositorService.isHyprland ? Hyprland : null
enabled: CompositorService.isHyprland enabled: CompositorService.isHyprland
@@ -661,39 +143,10 @@ Item {
} }
} }
FileView {
id: greeterWallpaperOverrideFile
path: GreetdSettings.greeterWallpaperOverridePath
printErrors: false
watchChanges: true
onLoaded: root.greeterWallpaperOverrideExists = true
onLoadFailed: root.greeterWallpaperOverrideExists = false
}
Connections {
target: GreetdSettings
function onGreeterWallpaperOverridePathChanged() {
if (!GreetdSettings.greeterWallpaperOverridePath) {
root.greeterWallpaperOverrideExists = false;
return;
}
greeterWallpaperOverrideFile.reload();
}
function onGreeterWallpaperPathChanged() {
if (!GreetdSettings.greeterWallpaperPath) {
root.greeterWallpaperOverrideExists = false;
return;
}
greeterWallpaperOverrideFile.reload();
}
}
DankBackdrop { DankBackdrop {
anchors.fill: parent anchors.fill: parent
screenName: root.screenName screenName: root.screenName
visible: { visible: {
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
return false;
var _ = SessionData.perMonitorWallpaper; var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers; var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName); var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
@@ -706,14 +159,12 @@ Item {
anchors.fill: parent anchors.fill: parent
source: { source: {
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
return encodeFileUrl(GreetdSettings.greeterWallpaperOverridePath);
var _ = SessionData.perMonitorWallpaper; var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers; var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName); var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : ""; return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
} }
fillMode: Theme.getFillMode(GreetdSettings.getEffectiveWallpaperFillMode()) fillMode: Theme.getFillMode(GreetdSettings.wallpaperFillMode)
smooth: true smooth: true
asynchronous: false asynchronous: false
cache: true cache: true
@@ -876,7 +327,10 @@ Item {
anchors.top: clockContainer.bottom anchors.top: clockContainer.bottom
anchors.topMargin: 4 anchors.topMargin: 4
text: { text: {
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat()); if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) {
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.lockDateFormat);
}
return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
} }
font.pixelSize: Theme.fontSizeXLarge font.pixelSize: Theme.fontSizeXLarge
color: "white" color: "white"
@@ -945,9 +399,6 @@ Item {
if (GreeterState.showPasswordInput && revealButton.visible) { if (GreeterState.showPasswordInput && revealButton.visible) {
margin += revealButton.width; margin += revealButton.width;
} }
if (externalAuthButton.visible) {
margin += externalAuthButton.width;
}
if (virtualKeyboardButton.visible) { if (virtualKeyboardButton.visible) {
margin += virtualKeyboardButton.width; margin += virtualKeyboardButton.width;
} }
@@ -964,18 +415,21 @@ Item {
return; return;
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
GreeterState.passwordBuffer = text; GreeterState.passwordBuffer = text;
if (!text || text.length === 0)
root.passwordSubmitRequested = false;
} else { } else {
GreeterState.usernameInput = text; GreeterState.usernameInput = text;
} }
} }
onAccepted: { onAccepted: {
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
root.startAuthSession(true); if (Greetd.state === GreetdState.Inactive && GreeterState.username) {
Greetd.createSession(GreeterState.username);
}
} else { } else {
if (text.trim()) { if (text.trim()) {
root.submitUsername(text); GreeterState.username = text.trim();
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
syncingFromState = true; syncingFromState = true;
text = ""; text = "";
syncingFromState = false; syncingFromState = false;
@@ -1007,14 +461,14 @@ Item {
anchors.left: lockIcon.right anchors.left: lockIcon.right
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))) anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))
anchors.rightMargin: 2 anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
if (GreeterState.unlocking) { if (GreeterState.unlocking) {
return "Logging in..."; return "Logging in...";
} }
if (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse) { if (Greetd.state !== GreetdState.Inactive) {
return "Authenticating..."; return "Authenticating...";
} }
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
@@ -1022,7 +476,7 @@ Item {
} }
return "Username..."; return "Username...";
} }
color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline color: GreeterState.unlocking ? Theme.primary : (Greetd.state !== GreetdState.Inactive ? Theme.primary : Theme.outline)
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0 opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
@@ -1044,7 +498,7 @@ Item {
StyledText { StyledText {
anchors.left: lockIcon.right anchors.left: lockIcon.right
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))) anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))
anchors.rightMargin: 2 anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
@@ -1074,27 +528,15 @@ Item {
DankActionButton { DankActionButton {
id: revealButton id: revealButton
anchors.right: externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)) anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
anchors.rightMargin: 0 anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: parent.showPassword ? "visibility_off" : "visibility" iconName: parent.showPassword ? "visibility_off" : "visibility"
buttonSize: 32 buttonSize: 32
visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
enabled: visible enabled: visible
onClicked: parent.showPassword = !parent.showPassword onClicked: parent.showPassword = !parent.showPassword
} }
DankActionButton {
id: externalAuthButton
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter
iconName: root.greeterPamHasFprint ? "fingerprint" : "key"
buttonSize: 32
visible: GreeterState.showPasswordInput && root.greeterExternalAuthAvailable && GreeterState.passwordBuffer.length === 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
enabled: visible
onClicked: root.startAuthSession(false)
}
DankActionButton { DankActionButton {
id: virtualKeyboardButton id: virtualKeyboardButton
@@ -1103,7 +545,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard" iconName: "keyboard"
buttonSize: 32 buttonSize: 32
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
enabled: visible enabled: visible
onClicked: { onClicked: {
if (keyboard_controller.isKeyboardActive) { if (keyboard_controller.isKeyboardActive) {
@@ -1122,14 +564,19 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard_return" iconName: "keyboard_return"
buttonSize: 36 buttonSize: 36
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
enabled: true enabled: true
onClicked: { onClicked: {
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
root.startAuthSession(true); if (GreeterState.username) {
Greetd.createSession(GreeterState.username);
}
} else { } else {
if (inputField.text.trim()) { if (inputField.text.trim()) {
root.submitUsername(inputField.text); GreeterState.username = inputField.text.trim();
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
inputField.text = ""; inputField.text = "";
} }
} }
@@ -1154,16 +601,20 @@ Item {
StyledText { StyledText {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 38 Layout.preferredHeight: 20
Layout.topMargin: -Theme.spacingS Layout.topMargin: -Theme.spacingS
Layout.bottomMargin: -Theme.spacingS Layout.bottomMargin: -Theme.spacingS
text: root.authFeedbackMessage text: {
if (GreeterState.pamState === "error")
return "Authentication error - try again";
if (GreeterState.pamState === "fail")
return "Incorrect password";
return "";
}
color: Theme.error color: Theme.error
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap opacity: GreeterState.pamState !== "" ? 1 : 0
maximumLineCount: 2
opacity: root.authFeedbackMessage !== "" ? 1 : 0
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
@@ -1216,7 +667,6 @@ Item {
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
onClicked: { onClicked: {
GreeterState.reset(); GreeterState.reset();
root.externalAuthAutoStartedForUser = "";
inputField.text = ""; inputField.text = "";
PortalService.profileImage = ""; PortalService.profileImage = "";
} }
@@ -1579,11 +1029,9 @@ Item {
return; return;
if (!GreetdMemory.memoryReady) if (!GreetdMemory.memoryReady)
return; return;
if (!GreetdSettings.settingsLoaded)
return;
const savedSession = GreetdSettings.rememberLastSession ? GreetdMemory.lastSessionId : ""; const savedSession = GreetdMemory.lastSessionId;
if (savedSession && GreetdSettings.rememberLastSession) { if (savedSession) {
for (var i = 0; i < GreeterState.sessionPaths.length; i++) { for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
if (GreeterState.sessionPaths[i] === savedSession) { if (GreeterState.sessionPaths[i] === savedSession) {
GreeterState.currentSessionIndex = i; GreeterState.currentSessionIndex = i;
@@ -1716,151 +1164,44 @@ Item {
function onAuthMessage(message, error, responseRequired, echoResponse) { function onAuthMessage(message, error, responseRequired, echoResponse) {
if (responseRequired) { if (responseRequired) {
cancelingExternalAuthForPassword = false; Greetd.respond(GreeterState.passwordBuffer);
passwordSessionTransitionRetryCount = 0; GreeterState.passwordBuffer = "";
awaitingExternalAuth = false; inputField.text = "";
pendingPasswordResponse = true;
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (!passwordSubmitRequested && hasPasswordBuffer)
passwordSubmitRequested = true;
if (passwordSubmitRequested && !root.submitBufferedPassword())
passwordSubmitRequested = false;
if (passwordSubmitRequested || hasPasswordBuffer) {
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
} else {
authTimeout.stop();
}
return; return;
} }
pendingPasswordResponse = false; if (!error)
const externalPrompt = root.isExternalAuthPrompt(message, responseRequired); Greetd.respond("");
if (!passwordSubmitRequested)
awaitingExternalAuth = root.greeterExternalAuthAvailable && externalPrompt;
if (awaitingExternalAuth || (passwordSubmitRequested && externalPrompt && root.greeterPamHasExternalAuth))
authTimeout.interval = externalAuthTimeoutMs;
else
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
Greetd.respond("");
}
function onStateChanged() {
if (Greetd.state === GreetdState.Inactive) {
const resumePasswordSubmit = cancelingExternalAuthForPassword && passwordSubmitRequested;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
cancelingExternalAuthForPassword = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
if (resumePasswordSubmit) {
Qt.callLater(function() {
root.startAuthSession(true);
});
return;
}
resetPasswordSessionTransition(true);
}
} }
function onReadyToLaunch() { function onReadyToLaunch() {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
passwordFailureCount = 0;
clearAuthFeedback();
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex]; const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex]; const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
if (!sessionCmd) { if (!sessionCmd) {
GreeterState.pamState = "error"; GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart(); placeholderDelay.restart();
return; return;
} }
GreeterState.unlocking = true; GreeterState.unlocking = true;
launchTimeout.restart(); launchTimeout.restart();
if (GreetdSettings.rememberLastSession) { GreetdMemory.setLastSessionId(sessionPath);
GreetdMemory.setLastSessionId(sessionPath); GreetdMemory.setLastSuccessfulUser(GreeterState.username);
} else if (GreetdMemory.lastSessionId) { Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
GreetdMemory.setLastSessionId("");
}
if (GreetdSettings.rememberLastUser) {
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
} else if (GreetdMemory.lastSuccessfulUser) {
GreetdMemory.setLastSuccessfulUser("");
}
pendingLaunchCommand = sessionCmd;
pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"];
memoryFlushTimer.restart();
} }
function onAuthFailure(message) { function onAuthFailure(message) {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
launchTimeout.stop(); launchTimeout.stop();
GreeterState.unlocking = false; GreeterState.unlocking = false;
if (isLikelyLockoutMessage(message)) { GreeterState.pamState = "fail";
GreeterState.pamState = "max";
} else {
GreeterState.pamState = "fail";
passwordFailureCount = passwordFailureCount + 1;
}
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = ""; GreeterState.passwordBuffer = "";
inputField.text = ""; inputField.text = "";
placeholderDelay.restart(); placeholderDelay.restart();
Greetd.cancelSession();
} }
function onError(error) { function onError(error) {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
launchTimeout.stop(); launchTimeout.stop();
GreeterState.unlocking = false; GreeterState.unlocking = false;
GreeterState.pamState = "error"; GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = "";
inputField.text = "";
placeholderDelay.restart();
Greetd.cancelSession();
}
}
Timer {
id: memoryFlushTimer
interval: memoryFlushDelayMs
onTriggered: {
if (!pendingLaunchCommand)
return;
const sessionCommand = pendingLaunchCommand;
const launchEnv = pendingLaunchEnv;
pendingLaunchCommand = "";
pendingLaunchEnv = [];
Greetd.launch(sessionCommand.split(" "), launchEnv);
}
}
Timer {
id: authTimeout
interval: defaultAuthTimeoutMs
onTriggered: {
if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive)
return;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = ""; GreeterState.passwordBuffer = "";
inputField.text = ""; inputField.text = "";
placeholderDelay.restart(); placeholderDelay.restart();
@@ -1874,11 +1215,8 @@ Item {
onTriggered: { onTriggered: {
if (!GreeterState.unlocking) if (!GreeterState.unlocking)
return; return;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
GreeterState.unlocking = false; GreeterState.unlocking = false;
GreeterState.pamState = "error"; GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart(); placeholderDelay.restart();
Greetd.cancelSession(); Greetd.cancelSession();
} }
@@ -1887,7 +1225,7 @@ Item {
Timer { Timer {
id: placeholderDelay id: placeholderDelay
interval: 4000 interval: 4000
onTriggered: clearAuthFeedback() onTriggered: GreeterState.pamState = ""
} }
LockPowerMenu { LockPowerMenu {
-2
View File
@@ -9,7 +9,6 @@ A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the
- **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc. - **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc.
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd` - **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
- **Session Memory**: Remembers last selected session and user - **Session Memory**: Remembers last selected session and user
- Can be disabled via `settings.json` keys: `greeterRememberLastSession` and `greeterRememberLastUser`
## Installation ## Installation
@@ -213,7 +212,6 @@ dms-greeter --command hyprland
dms-greeter --command sway dms-greeter --command sway
dms-greeter --command mangowc dms-greeter --command mangowc
dms-greeter --command niri -C /path/to/custom-niri.kdl dms-greeter --command niri -C /path/to/custom-niri.kdl
dms-greeter --command niri --remember-last-user false --remember-last-session false
``` ```
Configure greetd to use it in `/etc/greetd/config.toml`: Configure greetd to use it in `/etc/greetd/config.toml`:
+12 -162
View File
@@ -6,9 +6,6 @@ COMPOSITOR=""
COMPOSITOR_CONFIG="" COMPOSITOR_CONFIG=""
DMS_PATH="dms-greeter" DMS_PATH="dms-greeter"
CACHE_DIR="/var/cache/dms-greeter" CACHE_DIR="/var/cache/dms-greeter"
REMEMBER_LAST_SESSION=""
REMEMBER_LAST_USER=""
DEBUG_MODE=0
show_help() { show_help() {
cat << EOF cat << EOF
@@ -25,15 +22,6 @@ Options:
(default: dms-greeter) (default: dms-greeter)
--cache-dir PATH Cache directory for greeter data --cache-dir PATH Cache directory for greeter data
(default: /var/cache/dms-greeter) (default: /var/cache/dms-greeter)
--remember-last-session BOOL
Persist selected session to greeter memory
(BOOL: true/false, default: from settings.json)
--remember-last-user BOOL
Persist last successful username to greeter memory
(BOOL: true/false, default: from settings.json)
--no-save-session Alias for --remember-last-session false
--no-save-username Alias for --remember-last-user false
--debug Enable verbose startup logging to stderr
-h, --help Show this help message -h, --help Show this help message
Examples: Examples:
@@ -42,7 +30,6 @@ Examples:
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
dms-greeter --command niri --cache-dir /tmp/dmsgreeter dms-greeter --command niri --cache-dir /tmp/dmsgreeter
dms-greeter --command niri --no-save-session --no-save-username
dms-greeter --command mango dms-greeter --command mango
dms-greeter --command labwc dms-greeter --command labwc
EOF EOF
@@ -56,41 +43,6 @@ require_command() {
fi fi
} }
normalize_bool_flag() {
local flag_name="$1"
local value="$2"
local normalized="${value,,}"
case "$normalized" in
1|true|yes|on)
echo "1"
;;
0|false|no|off)
echo "0"
;;
*)
echo "Error: $flag_name must be true/false (or 1/0, yes/no, on/off)" >&2
exit 1
;;
esac
}
exec_compositor() {
local log_tag="$1"
shift
if [[ "$DEBUG_MODE" == "1" ]]; then
exec "$@"
fi
if command -v systemd-cat >/dev/null 2>&1; then
exec "$@" > >(systemd-cat -t "dms-greeter/$log_tag" -p info) 2>&1
fi
local log_file="$CACHE_DIR/$log_tag.log"
exec "$@" >> "$log_file" 2>&1
}
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
--command) --command)
@@ -109,26 +61,6 @@ while [[ $# -gt 0 ]]; do
CACHE_DIR="$2" CACHE_DIR="$2"
shift 2 shift 2
;; ;;
--remember-last-session)
REMEMBER_LAST_SESSION="$2"
shift 2
;;
--remember-last-user)
REMEMBER_LAST_USER="$2"
shift 2
;;
--no-save-session)
REMEMBER_LAST_SESSION="0"
shift
;;
--no-save-username)
REMEMBER_LAST_USER="0"
shift
;;
--debug)
DEBUG_MODE=1
shift
;;
-h|--help) -h|--help)
show_help show_help
exit 0 exit 0
@@ -179,71 +111,9 @@ export QT_QPA_PLATFORM=wayland
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1 export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
export EGL_PLATFORM=gbm export EGL_PLATFORM=gbm
export DMS_RUN_GREETER=1 export DMS_RUN_GREETER=1
ensure_cache_tree() {
local base="$1"
mkdir -p "$base/.local/state" "$base/.local/state/wireplumber" "$base/.local/share" "$base/.cache"
}
if ! ensure_cache_tree "$CACHE_DIR" 2>/dev/null; then
FALLBACK_CACHE_DIR="/tmp/dms-greeter-${UID:-$(id -u)}"
echo "Warning: cache directory '$CACHE_DIR' is not writable; falling back to '$FALLBACK_CACHE_DIR'" >&2
CACHE_DIR="$FALLBACK_CACHE_DIR"
if ! ensure_cache_tree "$CACHE_DIR"; then
echo "Error: failed to initialize fallback cache directory '$CACHE_DIR'" >&2
exit 1
fi
fi
export DMS_GREET_CFG_DIR="$CACHE_DIR" export DMS_GREET_CFG_DIR="$CACHE_DIR"
if [[ -n "$REMEMBER_LAST_SESSION" ]]; then mkdir -p "$CACHE_DIR"
DMS_GREET_REMEMBER_LAST_SESSION=$(normalize_bool_flag "--remember-last-session" "$REMEMBER_LAST_SESSION")
export DMS_GREET_REMEMBER_LAST_SESSION
if [[ "$DMS_GREET_REMEMBER_LAST_SESSION" == "1" ]]; then
DMS_SAVE_SESSION=true
else
DMS_SAVE_SESSION=false
fi
export DMS_SAVE_SESSION
fi
if [[ -n "$REMEMBER_LAST_USER" ]]; then
DMS_GREET_REMEMBER_LAST_USER=$(normalize_bool_flag "--remember-last-user" "$REMEMBER_LAST_USER")
export DMS_GREET_REMEMBER_LAST_USER
if [[ "$DMS_GREET_REMEMBER_LAST_USER" == "1" ]]; then
DMS_SAVE_USERNAME=true
else
DMS_SAVE_USERNAME=false
fi
export DMS_SAVE_USERNAME
fi
export HOME="$CACHE_DIR"
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
export XDG_DATA_HOME="$CACHE_DIR/.local/share"
export XDG_CACHE_HOME="$CACHE_DIR/.cache"
# Propagate correct XDG dirs into the systemd user session so socket-activated
# services (e.g. wireplumber) don't inherit HOME=/ from /etc/passwd.
if command -v systemctl >/dev/null 2>&1; then
systemctl --user set-environment \
HOME="$CACHE_DIR" \
XDG_STATE_HOME="$CACHE_DIR/.local/state" \
XDG_DATA_HOME="$CACHE_DIR/.local/share" \
XDG_CACHE_HOME="$CACHE_DIR/.cache" 2>/dev/null || true
if systemctl --user is-active --quiet wireplumber.service 2>/dev/null; then
systemctl --user restart wireplumber.service 2>/dev/null || true
fi
fi
# Keep greeter VT clean by default; callers can override via env or --debug.
if [[ -z "${RUST_LOG:-}" ]]; then
export RUST_LOG=warn
fi
if [[ -z "${NIRI_LOG:-}" ]]; then
export NIRI_LOG=warn
fi
if command -v qs >/dev/null 2>&1; then if command -v qs >/dev/null 2>&1; then
QS_BIN="qs" QS_BIN="qs"
@@ -260,9 +130,7 @@ if [[ "$DMS_PATH" == /* ]]; then
else else
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH") RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
if [[ "$DEBUG_MODE" == "1" ]]; then echo "Located DMS config at: $RESOLVED_PATH" >&2
echo "Located DMS config at: $RESOLVED_PATH" >&2
fi
QS_CMD="$QS_BIN -p $RESOLVED_PATH" QS_CMD="$QS_BIN -p $RESOLVED_PATH"
else else
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2 echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
@@ -283,10 +151,6 @@ hotkey-overlay {
environment { environment {
DMS_RUN_GREETER "1" DMS_RUN_GREETER "1"
HOME "$CACHE_DIR"
XDG_STATE_HOME "$CACHE_DIR/.local/state"
XDG_DATA_HOME "$CACHE_DIR/.local/share"
XDG_CACHE_HOME "$CACHE_DIR/.cache"
} }
debug { debug {
@@ -328,7 +192,7 @@ NIRI_EOF
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation" spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
NIRI_EOF NIRI_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG" exec niri -c "$COMPOSITOR_CONFIG"
;; ;;
hyprland) hyprland)
@@ -340,16 +204,11 @@ NIRI_EOF
TEMP_CONFIG=$(mktemp) TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << HYPRLAND_EOF cat > "$TEMP_CONFIG" << HYPRLAND_EOF
env = DMS_RUN_GREETER,1 env = DMS_RUN_GREETER,1
env = HOME,$CACHE_DIR
env = XDG_STATE_HOME,$CACHE_DIR/.local/state
env = XDG_DATA_HOME,$CACHE_DIR/.local/share
env = XDG_CACHE_HOME,$CACHE_DIR/.cache
misc { misc {
disable_hyprland_logo = true disable_hyprland_logo = true
} }
exec-once = systemctl --user import-environment HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME 2>/dev/null || true
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit" exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
@@ -358,19 +217,14 @@ HYPRLAND_EOF
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG" cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << HYPRLAND_EOF cat >> "$TEMP_CONFIG" << HYPRLAND_EOF
env = HOME,$CACHE_DIR
env = XDG_STATE_HOME,$CACHE_DIR/.local/state
env = XDG_DATA_HOME,$CACHE_DIR/.local/share
env = XDG_CACHE_HOME,$CACHE_DIR/.cache
exec-once = systemctl --user import-environment HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME 2>/dev/null || true
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit" exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
if command -v start-hyprland >/dev/null 2>&1; then if command -v start-hyprland >/dev/null 2>&1; then
exec_compositor "hyprland" start-hyprland -- --config "$COMPOSITOR_CONFIG" exec start-hyprland -- --config "$COMPOSITOR_CONFIG"
else else
exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG" exec Hyprland -c "$COMPOSITOR_CONFIG"
fi fi
;; ;;
@@ -379,7 +233,6 @@ HYPRLAND_EOF
if [[ -z "$COMPOSITOR_CONFIG" ]]; then if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp) TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << SWAY_EOF cat > "$TEMP_CONFIG" << SWAY_EOF
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
exec "$QS_CMD; swaymsg exit" exec "$QS_CMD; swaymsg exit"
SWAY_EOF SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
@@ -388,12 +241,11 @@ SWAY_EOF
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG" cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << SWAY_EOF cat >> "$TEMP_CONFIG" << SWAY_EOF
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
exec "$QS_CMD; swaymsg exit" exec "$QS_CMD; swaymsg exit"
SWAY_EOF SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG" exec sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
;; ;;
scroll) scroll)
@@ -401,7 +253,6 @@ SWAY_EOF
if [[ -z "$COMPOSITOR_CONFIG" ]]; then if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp) TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << SCROLL_EOF cat > "$TEMP_CONFIG" << SCROLL_EOF
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
exec "$QS_CMD; scrollmsg exit" exec "$QS_CMD; scrollmsg exit"
SCROLL_EOF SCROLL_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
@@ -410,12 +261,11 @@ SCROLL_EOF
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG" cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << SCROLL_EOF cat >> "$TEMP_CONFIG" << SCROLL_EOF
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
exec "$QS_CMD; scrollmsg exit" exec "$QS_CMD; scrollmsg exit"
SCROLL_EOF SCROLL_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG" exec scroll -c "$COMPOSITOR_CONFIG"
;; ;;
miracle|miracle-wm) miracle|miracle-wm)
@@ -435,24 +285,24 @@ exec "$QS_CMD; miraclemsg exit"
MIRACLE_EOF MIRACLE_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG" exec miracle-wm -c "$COMPOSITOR_CONFIG"
;; ;;
labwc) labwc)
require_command "labwc" require_command "labwc"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD" exec labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
else else
exec_compositor "labwc" labwc --session "$QS_CMD" exec labwc --session "$QS_CMD"
fi fi
;; ;;
mango|mangowc) mango|mangowc)
require_command "mango" require_command "mango"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit" exec mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
else else
exec_compositor "mango" mango -s "$QS_CMD && mmsg -d quit" exec mango -s "$QS_CMD && mmsg -d quit"
fi fi
;; ;;
+2 -17
View File
@@ -755,7 +755,7 @@ Item {
} }
} }
onAccepted: { onAccepted: {
if (!demoMode && !root.unlocking && !pam.passwd.active && !pam.u2fPending) { if (!demoMode && !pam.passwd.active && !pam.u2fPending) {
pam.passwd.start(); pam.passwd.start();
} }
} }
@@ -764,11 +764,6 @@ Item {
return; return;
} }
if (root.unlocking) {
event.accepted = true;
return;
}
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
if (pam.u2fPending) { if (pam.u2fPending) {
pam.cancelU2fPending(); pam.cancelU2fPending();
@@ -1022,7 +1017,7 @@ Item {
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending)) visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
enabled: !demoMode enabled: !demoMode
onClicked: { onClicked: {
if (!demoMode && !root.unlocking && !pam.u2fPending) { if (!demoMode && !pam.u2fPending) {
pam.passwd.start(); pam.passwd.start();
} }
} }
@@ -1631,7 +1626,6 @@ Item {
onStateChanged: { onStateChanged: {
root.pamState = state; root.pamState = state;
if (state !== "") { if (state !== "") {
root.unlocking = false;
placeholderDelay.restart(); placeholderDelay.restart();
passwordField.text = ""; passwordField.text = "";
root.passwordBuffer = ""; root.passwordBuffer = "";
@@ -1647,15 +1641,6 @@ Item {
} }
} }
Connections {
target: pam
function onUnlockInProgressChanged() {
if (!pam.unlockInProgress && root.unlocking)
root.unlocking = false;
}
}
Binding { Binding {
target: pam target: pam
property: "buffer" property: "buffer"
+36 -88
View File
@@ -25,29 +25,6 @@ Scope {
signal flashMsg signal flashMsg
signal unlockRequested signal unlockRequested
function resetAuthFlows(): void {
passwd.abort();
fprint.abort();
u2f.abort();
errorRetry.running = false;
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
passwdActiveTimeout.running = false;
unlockRequestTimeout.running = false;
u2fPending = false;
u2fState = "";
unlockInProgress = false;
}
function recoverFromAuthStall(newState: string): void {
resetAuthFlows();
state = newState;
flashMsg();
stateReset.restart();
fprint.checkAvail();
u2f.checkAvail();
}
function completeUnlock(): void { function completeUnlock(): void {
if (!unlockInProgress) { if (!unlockInProgress) {
unlockInProgress = true; unlockInProgress = true;
@@ -59,7 +36,6 @@ Scope {
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
u2fPending = false; u2fPending = false;
u2fState = ""; u2fState = "";
unlockRequestTimeout.restart();
unlockRequested(); unlockRequested();
} }
} }
@@ -90,13 +66,6 @@ Scope {
printErrors: false printErrors: false
} }
FileView {
id: loginConfigWatcher
path: "/etc/pam.d/login"
printErrors: false
}
FileView { FileView {
id: u2fConfigWatcher id: u2fConfigWatcher
@@ -108,7 +77,7 @@ Scope {
id: passwd id: passwd
config: dankshellConfigWatcher.loaded ? "dankshell" : "login" config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
configDirectory: dankshellConfigWatcher.loaded || loginConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam" configDirectory: dankshellConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
onMessageChanged: { onMessageChanged: {
if (message.startsWith("The account is locked")) if (message.startsWith("The account is locked"))
@@ -133,13 +102,6 @@ Scope {
return; return;
} }
unlockRequestTimeout.running = false;
root.unlockInProgress = false;
root.u2fPending = false;
root.u2fState = "";
u2fPendingTimeout.running = false;
u2f.abort();
if (res === PamResult.Error) if (res === PamResult.Error)
root.state = "error"; root.state = "error";
else if (res === PamResult.MaxTries) else if (res === PamResult.MaxTries)
@@ -152,22 +114,10 @@ Scope {
} }
} }
Connections {
target: passwd
function onActiveChanged() {
if (passwd.active) {
passwdActiveTimeout.restart();
} else {
passwdActiveTimeout.running = false;
}
}
}
PamContext { PamContext {
id: fprint id: fprint
property bool available: SettingsData.lockFingerprintReady property bool available
property int tries property int tries
property int errorTries property int errorTries
@@ -223,7 +173,7 @@ Scope {
PamContext { PamContext {
id: u2f id: u2f
property bool available: SettingsData.lockU2fReady property bool available
function checkAvail(): void { function checkAvail(): void {
if (!available || !SettingsData.enableU2f || !root.lockSecured) { if (!available || !SettingsData.enableU2f || !root.lockSecured) {
@@ -252,7 +202,7 @@ Scope {
configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam" configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
onMessageChanged: { onMessageChanged: {
if (message !== "") if (message.toLowerCase().includes("touch"))
root.u2fState = "waiting"; root.u2fState = "waiting";
} }
@@ -288,6 +238,26 @@ Scope {
} }
} }
Process {
id: availProc
command: ["sh", "-c", "fprintd-list $USER"]
onExited: code => {
fprint.available = code === 0;
fprint.checkAvail();
}
}
Process {
id: u2fAvailProc
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\")"]
onExited: code => {
u2f.available = code === 0;
u2f.checkAvail();
}
}
Timer { Timer {
id: errorRetry id: errorRetry
@@ -309,26 +279,6 @@ Scope {
onTriggered: root.cancelU2fPending() onTriggered: root.cancelU2fPending()
} }
Timer {
id: passwdActiveTimeout
interval: 15000
onTriggered: {
if (passwd.active)
root.recoverFromAuthStall("error");
}
}
Timer {
id: unlockRequestTimeout
interval: 8000
onTriggered: {
if (root.unlockInProgress)
root.recoverFromAuthStall("error");
}
}
Timer { Timer {
id: stateReset id: stateReset
@@ -351,17 +301,24 @@ Scope {
onLockSecuredChanged: { onLockSecuredChanged: {
if (lockSecured) { if (lockSecured) {
SettingsData.refreshAuthAvailability(); availProc.running = true;
u2fAvailProc.running = true;
root.state = ""; root.state = "";
root.fprintState = ""; root.fprintState = "";
root.u2fState = ""; root.u2fState = "";
root.u2fPending = false; root.u2fPending = false;
root.lockMessage = ""; root.lockMessage = "";
root.resetAuthFlows(); root.unlockInProgress = false;
fprint.checkAvail();
u2f.checkAvail();
} else { } else {
root.resetAuthFlows(); fprint.abort();
passwd.abort();
u2f.abort();
errorRetry.running = false;
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
root.u2fPending = false;
root.u2fState = "";
root.unlockInProgress = false;
} }
} }
@@ -372,24 +329,15 @@ Scope {
fprint.checkAvail(); fprint.checkAvail();
} }
function onLockFingerprintReadyChanged(): void {
fprint.checkAvail();
}
function onEnableU2fChanged(): void { function onEnableU2fChanged(): void {
u2f.checkAvail(); u2f.checkAvail();
} }
function onLockU2fReadyChanged(): void {
u2f.checkAvail();
}
function onU2fModeChanged(): void { function onU2fModeChanged(): void {
if (root.lockSecured) { if (root.lockSecured) {
u2f.abort(); u2f.abort();
u2fErrorRetry.running = false; u2fErrorRetry.running = false;
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
unlockRequestTimeout.running = false;
root.u2fPending = false; root.u2fPending = false;
root.u2fState = ""; root.u2fState = "";
u2f.checkAvail(); u2f.checkAvail();
@@ -7,7 +7,7 @@ DankPopout {
id: root id: root
layerNamespace: "dms:notification-center-popout" layerNamespace: "dms:notification-center-popout"
fullHeightSurface: true fullHeightSurface: false
property bool notificationHistoryVisible: false property bool notificationHistoryVisible: false
property var triggerScreen: null property var triggerScreen: null
@@ -39,11 +39,9 @@ DankPopout {
} }
} }
popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400 popupWidth: 400
popupHeight: stablePopupHeight popupHeight: stablePopupHeight
positioning: "" positioning: ""
animationScaleCollapsed: 0.94
animationOffset: 0
suspendShadowWhileResizing: false suspendShadowWhileResizing: false
screen: triggerScreen screen: triggerScreen
@@ -24,6 +24,29 @@ PanelWindow {
property real _lastReportedAlignedHeight: -1 property real _lastReportedAlignedHeight: -1
property real _storedTopMargin: 0 property real _storedTopMargin: 0
property real _storedBottomMargin: 0 property real _storedBottomMargin: 0
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real entryTravel: {
const base = Math.abs(Theme.effectAnimOffset);
if (directionalEffect) {
if (isCenterPosition)
return Math.max(base, Math.round(content.height * 1.1));
return Math.max(base, Math.round(content.width * 0.95));
}
if (depthEffect)
return Math.max(base, 44);
return base;
}
readonly property real exitTravel: {
if (directionalEffect) {
if (isCenterPosition)
return content.height + entryTravel;
return content.width + entryTravel;
}
if (depthEffect)
return Math.round(entryTravel * 1.35);
return Anims.slidePx;
}
readonly property string clearText: I18n.tr("Dismiss") readonly property string clearText: I18n.tr("Dismiss")
property bool descriptionExpanded: false property bool descriptionExpanded: false
readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0 readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
@@ -137,9 +160,9 @@ PanelWindow {
enabled: !exiting && !_isDestroying enabled: !exiting && !_isDestroying
NumberAnimation { NumberAnimation {
id: implicitHeightAnim id: implicitHeightAnim
duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration duration: Theme.variantDuration(descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration, descriptionExpanded)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasized easing.bezierCurve: descriptionExpanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
@@ -911,9 +934,9 @@ PanelWindow {
if (isCenterPosition) if (isCenterPosition)
return 0; return 0;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx; return isLeft ? -entryTravel : entryTravel;
} }
y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0 y: isTopCenter ? -entryTravel : isBottomCenter ? entryTravel : 0
} }
] ]
} }
@@ -925,16 +948,16 @@ PanelWindow {
property: isCenterPosition ? "y" : "x" property: isCenterPosition ? "y" : "x"
from: { from: {
if (isTopCenter) if (isTopCenter)
return -Anims.slidePx; return -entryTravel;
if (isBottomCenter) if (isBottomCenter)
return Anims.slidePx; return entryTravel;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx; return isLeft ? -entryTravel : entryTravel;
} }
to: 0 to: 0
duration: Theme.notificationEnterDuration duration: Theme.variantDuration(Theme.notificationEnterDuration, true)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel easing.bezierCurve: Theme.variantPopoutEnterCurve
onStopped: { onStopped: {
if (!win.exiting && !win._isDestroying) { if (!win.exiting && !win._isDestroying) {
if (isCenterPosition) { if (isCenterPosition) {
@@ -959,35 +982,35 @@ PanelWindow {
from: 0 from: 0
to: { to: {
if (isTopCenter) if (isTopCenter)
return -Anims.slidePx; return -exitTravel;
if (isBottomCenter) if (isBottomCenter)
return Anims.slidePx; return exitTravel;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx; return isLeft ? -exitTravel : exitTravel;
} }
duration: Theme.notificationExitDuration duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel easing.bezierCurve: Theme.variantPopoutExitCurve
} }
NumberAnimation { NumberAnimation {
target: content target: content
property: "opacity" property: "opacity"
from: 1 from: 1
to: 0 to: Theme.isDirectionalEffect ? 1 : 0
duration: Theme.notificationExitDuration duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.standardAccel easing.bezierCurve: Theme.variantPopoutExitCurve
} }
NumberAnimation { NumberAnimation {
target: content target: content
property: "scale" property: "scale"
from: 1 from: 1
to: 0.98 to: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed
duration: Theme.notificationExitDuration duration: Theme.variantDuration(Theme.notificationExitDuration, false)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel easing.bezierCurve: Theme.variantPopoutExitCurve
} }
} }

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