mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-03 02:52:07 -04:00
Compare commits
2 Commits
8d0f256f74
...
anims
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62bf9c6efe | ||
|
|
61a77bd186 |
2
.github/workflows/dms-stable.yml
vendored
2
.github/workflows/dms-stable.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app_token.outputs.token }}
|
||||
|
||||
4
.github/workflows/go-ci.yml
vendored
4
.github/workflows/go-ci.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install 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
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
|
||||
2
.github/workflows/nix-pr-check.yml
vendored
2
.github/workflows/nix-pr-check.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
4
.github/workflows/prek.yml
vendored
4
.github/workflows/prek.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install 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
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: core/go.mod
|
||||
|
||||
|
||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -32,13 +32,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
|
||||
- name: Upload artifacts (${{ matrix.arch }})
|
||||
if: matrix.arch == 'arm64'
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: core-assets-${{ matrix.arch }}
|
||||
path: |
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
|
||||
- name: Upload artifacts with completions
|
||||
if: matrix.arch == 'amd64'
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: core-assets-${{ matrix.arch }}
|
||||
path: |
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v6
|
||||
# uses: actions/checkout@v4
|
||||
# with:
|
||||
# token: ${{ steps.app_token.outputs.token }}
|
||||
# fetch-depth: 0
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
TAG: ${{ inputs.tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
@@ -192,12 +192,12 @@ jobs:
|
||||
git checkout ${TAG}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
- name: Download core artifacts
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: core-assets-*
|
||||
merge-multiple: true
|
||||
|
||||
4
.github/workflows/run-copr.yml
vendored
4
.github/workflows/run-copr.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
rpm -qpi "$SRPM"
|
||||
|
||||
- name: Upload SRPM artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
||||
path: ${{ steps.build.outputs.srpm_path }}
|
||||
|
||||
9
.github/workflows/run-obs.yml
vendored
9
.github/workflows/run-obs.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -195,13 +195,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Wait before OBS upload
|
||||
run: sleep 3
|
||||
|
||||
- name: Determine packages to update
|
||||
id: packages
|
||||
run: |
|
||||
@@ -347,7 +344,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
|
||||
6
.github/workflows/run-ppa.yml
vendored
6
.github/workflows/run-ppa.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -157,12 +157,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
cache: false
|
||||
|
||||
8
.github/workflows/update-vendor-hash.yml
vendored
8
.github/workflows/update-vendor-hash.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app_token.outputs.token }}
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
echo "Build succeeded, no hash update needed"
|
||||
exit 0
|
||||
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; }
|
||||
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
||||
[ "$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 add flake.nix
|
||||
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
||||
git pull --rebase origin ${{ github.ref_name }}
|
||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
|
||||
git pull --rebase origin master
|
||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
||||
else
|
||||
echo "No changes to flake.nix"
|
||||
fi
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
This file is more of a quick reference so I know what to account for before next releases.
|
||||
|
||||
# 1.5.0
|
||||
- Overhauled shadows
|
||||
- App ID changed to com.danklinux.dms - breaking for window rules
|
||||
- Greeter stuff
|
||||
- Terminal mux
|
||||
- Locale overrides
|
||||
- new neovim theming
|
||||
|
||||
# 1.4.0
|
||||
|
||||
- Overhauled system monitor, graphs, styling
|
||||
|
||||
@@ -86,9 +86,7 @@ touch .qmlls.ini
|
||||
|
||||
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`.
|
||||
|
||||
6. Make your changes, test, and open a pull request.
|
||||
5. Make your changes, test, and open a pull request.
|
||||
|
||||
### I18n/Localization
|
||||
|
||||
|
||||
8
Makefile
8
Makefile
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
||||
ASSETS_DIR=assets
|
||||
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
|
||||
|
||||
@@ -32,9 +32,6 @@ clean:
|
||||
@$(MAKE) -C $(CORE_DIR) clean
|
||||
@echo "Clean complete"
|
||||
|
||||
lint-qml:
|
||||
@./quickshell/scripts/qmllint-entrypoints.sh
|
||||
|
||||
# Installation targets
|
||||
install-bin:
|
||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||
@@ -79,7 +76,7 @@ install-desktop:
|
||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||
@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 "Installation complete!"
|
||||
@echo ""
|
||||
@@ -133,7 +130,6 @@ help:
|
||||
@echo " all (default) - Build the DMS binary"
|
||||
@echo " build - Same as 'all'"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS"
|
||||
@echo ""
|
||||
@echo "Install:"
|
||||
@echo " install - Build and install everything (requires sudo)"
|
||||
|
||||
@@ -1,26 +1,13 @@
|
||||
repos:
|
||||
- repo: local
|
||||
- repo: https://github.com/golangci/golangci-lint
|
||||
rev: v2.10.1
|
||||
hooks:
|
||||
- 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
|
||||
types: [go]
|
||||
pass_filenames: false
|
||||
- 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
|
||||
name: golangci-lint-config-verify
|
||||
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 config verify
|
||||
language: system
|
||||
files: \.golangci\.(?:yml|yaml|toml|json)
|
||||
pass_filenames: false
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: go-test
|
||||
name: go test
|
||||
entry: go test ./...
|
||||
|
||||
@@ -63,19 +63,19 @@ endif
|
||||
|
||||
build-all: build dankinstall
|
||||
|
||||
install:
|
||||
install: build
|
||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||
@echo "Installation complete"
|
||||
|
||||
install-all:
|
||||
install-all: build-all
|
||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||
@echo "Installation complete"
|
||||
|
||||
install-dankinstall:
|
||||
install-dankinstall: dankinstall
|
||||
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||
@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."
|
||||
}
|
||||
@@ -222,19 +222,16 @@ func init() {
|
||||
|
||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||
var data []byte
|
||||
copyFromStdin := false
|
||||
|
||||
switch {
|
||||
case len(args) > 0:
|
||||
data = []byte(args[0])
|
||||
case clipCopyDownload || clipCopyType == "__multi__":
|
||||
default:
|
||||
var err error
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
log.Fatalf("read stdin: %v", err)
|
||||
}
|
||||
default:
|
||||
copyFromStdin = true
|
||||
}
|
||||
|
||||
if clipCopyDownload {
|
||||
@@ -260,13 +257,6 @@ func runClipCopy(cmd *cobra.Command, args []string) {
|
||||
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 {
|
||||
log.Fatalf("copy: %v", err)
|
||||
}
|
||||
|
||||
@@ -1079,14 +1079,14 @@ func formatResultsPlain(results []checkResult) string {
|
||||
if currentCategory != -1 {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
fmt.Fprintf(&sb, "**%s**\n", r.category.String())
|
||||
sb.WriteString(fmt.Sprintf("**%s**\n", r.category.String()))
|
||||
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 != "" {
|
||||
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")
|
||||
fmt.Fprintf(&sb, "**Summary:** %d error(s), %d warning(s), %d ok\n",
|
||||
ds.ErrorCount(), ds.WarningCount(), ds.OKCount())
|
||||
sb.WriteString(fmt.Sprintf("**Summary:** %d error(s), %d warning(s), %d ok\n",
|
||||
ds.ErrorCount(), ds.WarningCount(), ds.OKCount()))
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -57,11 +57,10 @@ func init() {
|
||||
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
||||
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
||||
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
|
||||
cmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
|
||||
}
|
||||
|
||||
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
||||
matugenQueueCmd.Flags().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 {
|
||||
@@ -78,7 +77,6 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
||||
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
||||
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
||||
contrast, _ := cmd.Flags().GetFloat64("contrast")
|
||||
|
||||
return matugen.Options{
|
||||
StateDir: stateDir,
|
||||
@@ -89,7 +87,6 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||
Mode: matugen.ColorMode(mode),
|
||||
IconTheme: iconTheme,
|
||||
MatugenType: matugenType,
|
||||
Contrast: contrast,
|
||||
RunUserTemplates: runUserTemplates,
|
||||
StockColors: stockColors,
|
||||
SyncModeWithPortal: syncModeWithPortal,
|
||||
@@ -131,7 +128,6 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
||||
"syncModeWithPortal": opts.SyncModeWithPortal,
|
||||
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
||||
"skipTemplates": opts.SkipTemplates,
|
||||
"contrast": opts.Contrast,
|
||||
"wait": wait,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,10 +16,9 @@ import (
|
||||
)
|
||||
|
||||
var setupCmd = &cobra.Command{
|
||||
Use: "setup",
|
||||
Short: "Deploy DMS configurations",
|
||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||
PersistentPreRunE: requireMutableSystemCommand,
|
||||
Use: "setup",
|
||||
Short: "Deploy DMS configurations",
|
||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runSetup(); err != nil {
|
||||
log.Fatalf("Error during setup: %v", err)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -16,10 +16,19 @@ func init() {
|
||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||
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)
|
||||
|
||||
// Add subcommands to update
|
||||
updateCmd.AddCommand(updateCheckCmd)
|
||||
|
||||
// Add subcommands to plugins
|
||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||
|
||||
// Add common commands to root
|
||||
rootCmd.AddCommand(getCommonCommands()...)
|
||||
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
|
||||
@@ -11,20 +11,29 @@ import (
|
||||
var Version = "dev"
|
||||
|
||||
func init() {
|
||||
// Add flags
|
||||
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("session", false, "Session managed (like as a systemd unit)")
|
||||
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)
|
||||
|
||||
// Add subcommands to plugins
|
||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||
|
||||
// Add common commands to root
|
||||
rootCmd.AddCommand(getCommonCommands()...)
|
||||
|
||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Block root
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("This program should not be run as root. Exiting.")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,14 @@ import (
|
||||
"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 {
|
||||
cmd := exec.Command("pacman", "-Q", packageName)
|
||||
err := cmd.Run()
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
module github.com/AvengeMedia/DankMaterialShell/core
|
||||
|
||||
go 1.26.0
|
||||
|
||||
toolchain go1.26.1
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||
@@ -14,37 +12,17 @@ import (
|
||||
)
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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"}
|
||||
if pasteOnce {
|
||||
args = append(args, "--paste-once")
|
||||
@@ -52,15 +30,11 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||
args = append(args, "--type", mimeType)
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdin = nil
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
|
||||
if stdinSource, ok := data.(*os.File); ok {
|
||||
cmd.Stdin = stdinSource
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(stdin, data); err != nil {
|
||||
if _, err := stdin.Write(data); err != nil {
|
||||
stdin.Close()
|
||||
return fmt.Errorf("write stdin: %w", err)
|
||||
}
|
||||
if err := stdin.Close(); err != nil {
|
||||
return fmt.Errorf("close stdin: %w", err)
|
||||
}
|
||||
stdin.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyServeReader(data io.Reader, 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 {
|
||||
func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
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{})
|
||||
pasted := make(chan struct{}, 1)
|
||||
sendErr := make(chan error, 1)
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
defer syscall.Close(e.Fd)
|
||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||
defer file.Close()
|
||||
if err := writeTo(file); err != nil {
|
||||
select {
|
||||
case sendErr <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
file.Write(data)
|
||||
select {
|
||||
case pasted <- struct{}{}:
|
||||
default:
|
||||
@@ -247,8 +165,6 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
||||
select {
|
||||
case <-cancelled:
|
||||
return nil
|
||||
case err := <-sendErr:
|
||||
return err
|
||||
case <-pasted:
|
||||
if pasteOnce {
|
||||
return nil
|
||||
|
||||
@@ -252,7 +252,6 @@ window-rule {
|
||||
// Open dms windows as floating by default
|
||||
window-rule {
|
||||
match app-id=r#"org.quickshell$"#
|
||||
match app-id=r#"com.danklinux.dms$"#
|
||||
open-floating true
|
||||
}
|
||||
debug {
|
||||
|
||||
@@ -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, ", ")))
|
||||
|
||||
hasNiri := false
|
||||
hasQuickshell := false
|
||||
for _, pkg := range packages {
|
||||
if pkg == "niri-git" {
|
||||
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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||
if pkg == "dms-shell-bin" {
|
||||
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),
|
||||
}
|
||||
} else {
|
||||
// Skip dependency installation for dms-shell-git and dms-shell-bin
|
||||
// since we manually manage those dependencies
|
||||
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
|
||||
// Pre-install dependencies from .SRCINFO
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
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",
|
||||
}
|
||||
|
||||
// Install dependencies from .SRCINFO
|
||||
depFilter := ""
|
||||
if pkg == "dms-shell-git" {
|
||||
depFilter = ` | sed -E 's/[[:space:]]*(quickshell|dgop)[[:space:]]*/ /g' | tr -s ' '`
|
||||
}
|
||||
// Install dependencies and makedepends explicitly
|
||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||
|
||||
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
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
|
||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
||||
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 {
|
||||
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 {
|
||||
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{
|
||||
@@ -656,7 +677,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
||||
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 {
|
||||
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
@@ -91,27 +92,9 @@ func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||
return debianPackageInstalledPrecisely(pkg)
|
||||
}
|
||||
|
||||
func debianPackageInstalledPrecisely(pkg string) bool {
|
||||
cmd := exec.Command("dpkg-query", "-W", "-f=${db:Status-Status}", pkg)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(string(output)) == "installed"
|
||||
}
|
||||
|
||||
func debianRepoArchitecture(arch string) string {
|
||||
switch arch {
|
||||
case "amd64", "x86_64":
|
||||
return "amd64"
|
||||
case "arm64", "aarch64":
|
||||
return "arm64"
|
||||
default:
|
||||
return arch
|
||||
}
|
||||
cmd := exec.Command("dpkg", "-l", pkg)
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
@@ -211,12 +194,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
Step: "Installing development dependencies...",
|
||||
IsComplete: false,
|
||||
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",
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("failed to install development tools: %w", err)
|
||||
}
|
||||
@@ -396,14 +379,6 @@ func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []st
|
||||
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 {
|
||||
enabledRepos := make(map[string]bool)
|
||||
|
||||
@@ -461,7 +436,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
||||
}
|
||||
|
||||
// Add repository
|
||||
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, debianRepoArchitecture(osInfo.Architecture), baseURL)
|
||||
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
@@ -507,46 +482,20 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
|
||||
|
||||
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
groups := orderedMinimalInstallGroups(packages)
|
||||
totalGroups := len(groups)
|
||||
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
|
||||
groupIndex := 0
|
||||
installGroup := func(groupPackages []string, minimal bool) error {
|
||||
if len(groupPackages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
groupIndex++
|
||||
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)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
|
||||
@@ -484,7 +484,28 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, 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 {
|
||||
@@ -494,57 +515,26 @@ func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, 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"}
|
||||
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...)
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
||||
groups := orderedMinimalInstallGroups(packages)
|
||||
totalGroups := len(groups)
|
||||
|
||||
groupIndex := 0
|
||||
installGroup := func(groupPackages []string, minimal bool) error {
|
||||
if len(groupPackages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
groupIndex++
|
||||
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
|
||||
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.70,
|
||||
Step: "Installing COPR 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, PhaseAURPackages, 0.70, 0.85)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
@@ -30,8 +29,6 @@ type OpenSUSEDistribution struct {
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
const openSUSENiriWaylandServerPackage = "libwayland-server0"
|
||||
|
||||
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
||||
base := NewBaseDistribution(logChan)
|
||||
return &OpenSUSEDistribution{
|
||||
@@ -202,7 +199,35 @@ func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -272,10 +297,6 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
||||
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 {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
@@ -306,7 +327,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
||||
NeedsSudo: true,
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -321,7 +342,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
||||
IsComplete: false,
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -411,32 +432,9 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
|
||||
}
|
||||
}
|
||||
|
||||
systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags))
|
||||
|
||||
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
||||
}
|
||||
|
||||
func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string]bool) []string {
|
||||
if wm != deps.WindowManagerNiri || disabledFlags["niri"] {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []string{openSUSENiriWaylandServerPackage}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
|
||||
for _, pkg := range extraPkgs {
|
||||
if slices.Contains(systemPkgs, pkg) || o.packageInstalled(pkg) {
|
||||
continue
|
||||
}
|
||||
|
||||
o.log(fmt.Sprintf("Adding openSUSE runtime package: %s", pkg))
|
||||
systemPkgs = append(systemPkgs, pkg)
|
||||
}
|
||||
|
||||
return systemPkgs
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||
names := make([]string, len(packages))
|
||||
for i, pkg := range packages {
|
||||
@@ -516,146 +514,27 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
||||
return nil
|
||||
}
|
||||
|
||||
func isOpenSUSEInstallMediaURI(uri string) bool {
|
||||
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 {
|
||||
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
groups := orderedMinimalInstallGroups(packages)
|
||||
totalGroups := len(groups)
|
||||
args := []string{"zypper", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
|
||||
groupIndex := 0
|
||||
installGroup := func(groupPackages []string, minimal bool) error {
|
||||
if len(groupPackages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
groupIndex++
|
||||
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)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
|
||||
@@ -100,7 +100,9 @@ func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -452,7 +454,21 @@ func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, 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 {
|
||||
@@ -461,59 +477,21 @@ func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, 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{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||
if minimal {
|
||||
args = append(args, "--no-install-recommends")
|
||||
}
|
||||
return append(args, packages...)
|
||||
}
|
||||
args := []string{"apt-get", "install", "-y"}
|
||||
args = 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 {
|
||||
groups := orderedMinimalInstallGroups(packages)
|
||||
totalGroups := len(groups)
|
||||
|
||||
groupIndex := 0
|
||||
installGroup := func(groupPackages []string, minimal bool) error {
|
||||
if len(groupPackages) == 0 {
|
||||
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)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.70,
|
||||
Step: "Installing PPA packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
||||
}
|
||||
|
||||
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,98 +0,0 @@
|
||||
package greeter
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeTestJSON(t *testing.T, path string, content string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatalf("failed to create parent dir for %s: %v", path, err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("failed to write %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveGreeterThemeSyncState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
settingsJSON string
|
||||
sessionJSON string
|
||||
wantSourcePath string
|
||||
wantResolvedWallpaper string
|
||||
wantDynamicOverrideUsed bool
|
||||
}{
|
||||
{
|
||||
name: "dynamic theme with greeter wallpaper override uses generated greeter colors",
|
||||
settingsJSON: `{
|
||||
"currentThemeName": "dynamic",
|
||||
"greeterWallpaperPath": "Pictures/blue.jpg",
|
||||
"matugenScheme": "scheme-tonal-spot",
|
||||
"iconTheme": "Papirus"
|
||||
}`,
|
||||
sessionJSON: `{"isLightMode":true}`,
|
||||
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "greeter-colors", "dms-colors.json"),
|
||||
wantResolvedWallpaper: filepath.Join("Pictures", "blue.jpg"),
|
||||
wantDynamicOverrideUsed: true,
|
||||
},
|
||||
{
|
||||
name: "dynamic theme without override uses desktop colors",
|
||||
settingsJSON: `{
|
||||
"currentThemeName": "dynamic",
|
||||
"greeterWallpaperPath": ""
|
||||
}`,
|
||||
sessionJSON: `{"isLightMode":false}`,
|
||||
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
||||
wantResolvedWallpaper: "",
|
||||
wantDynamicOverrideUsed: false,
|
||||
},
|
||||
{
|
||||
name: "non-dynamic theme keeps desktop colors even with override wallpaper",
|
||||
settingsJSON: `{
|
||||
"currentThemeName": "purple",
|
||||
"greeterWallpaperPath": "/tmp/blue.jpg"
|
||||
}`,
|
||||
sessionJSON: `{"isLightMode":false}`,
|
||||
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
||||
wantResolvedWallpaper: "/tmp/blue.jpg",
|
||||
wantDynamicOverrideUsed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
homeDir := t.TempDir()
|
||||
writeTestJSON(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
|
||||
writeTestJSON(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
|
||||
|
||||
state, err := resolveGreeterThemeSyncState(homeDir)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveGreeterThemeSyncState returned error: %v", err)
|
||||
}
|
||||
|
||||
if got := state.effectiveColorsSource(homeDir); got != filepath.Join(homeDir, tt.wantSourcePath) {
|
||||
t.Fatalf("effectiveColorsSource = %q, want %q", got, filepath.Join(homeDir, tt.wantSourcePath))
|
||||
}
|
||||
|
||||
wantResolvedWallpaper := tt.wantResolvedWallpaper
|
||||
if wantResolvedWallpaper != "" && !filepath.IsAbs(wantResolvedWallpaper) {
|
||||
wantResolvedWallpaper = filepath.Join(homeDir, wantResolvedWallpaper)
|
||||
}
|
||||
if state.ResolvedGreeterWallpaperPath != wantResolvedWallpaper {
|
||||
t.Fatalf("ResolvedGreeterWallpaperPath = %q, want %q", state.ResolvedGreeterWallpaperPath, wantResolvedWallpaper)
|
||||
}
|
||||
|
||||
if state.UsesDynamicWallpaperOverride != tt.wantDynamicOverrideUsed {
|
||||
t.Fatalf("UsesDynamicWallpaperOverride = %v, want %v", state.UsesDynamicWallpaperOverride, tt.wantDynamicOverrideUsed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,6 @@ var templateRegistry = []TemplateDef{
|
||||
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
||||
{ID: "vscode", Kind: TemplateKindVSCode},
|
||||
{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 {
|
||||
@@ -99,9 +98,7 @@ type Options struct {
|
||||
Mode ColorMode
|
||||
IconTheme string
|
||||
MatugenType string
|
||||
Contrast float64
|
||||
RunUserTemplates bool
|
||||
ColorsOnly bool
|
||||
StockColors string
|
||||
SyncModeWithPortal bool
|
||||
TerminalsAlwaysDark bool
|
||||
@@ -229,7 +226,6 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
|
||||
log.Info("Running matugen color hex with stock color overrides")
|
||||
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
||||
args = appendContrastArg(args, opts.Contrast)
|
||||
args = append(args, importArgs...)
|
||||
if err := runMatugen(args); err != nil {
|
||||
return false, err
|
||||
@@ -266,7 +262,6 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
args = []string{opts.Kind, opts.Value}
|
||||
}
|
||||
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
||||
args = appendContrastArg(args, opts.Contrast)
|
||||
args = append(args, importArgs...)
|
||||
if err := runMatugen(args); err != nil {
|
||||
return false, err
|
||||
@@ -278,10 +273,6 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if opts.ColorsOnly {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if isDMSGTKActive(opts.ConfigDir) {
|
||||
switch opts.Mode {
|
||||
case ColorModeLight:
|
||||
@@ -302,13 +293,6 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func appendContrastArg(args []string, contrast float64) []string {
|
||||
if contrast == 0 {
|
||||
return args
|
||||
}
|
||||
return append(args, "--contrast", strconv.FormatFloat(contrast, 'f', -1, 64))
|
||||
}
|
||||
|
||||
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
||||
userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml")
|
||||
|
||||
@@ -346,10 +330,6 @@ output_path = '%s'
|
||||
|
||||
`, opts.ShellDir, opts.ColorsOutput())
|
||||
|
||||
if opts.ColorsOnly {
|
||||
return nil
|
||||
}
|
||||
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
for _, tmpl := range templateRegistry {
|
||||
if opts.ShouldSkipTemplate(tmpl.ID) {
|
||||
@@ -616,10 +596,10 @@ func detectMatugenVersionLocked() (matugenFlags, error) {
|
||||
matugenVersionOK = true
|
||||
|
||||
if matugenSupportsCOE {
|
||||
log.Debugf("Matugen %s detected: continue-on-error support enabled", versionStr)
|
||||
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
||||
}
|
||||
if matugenIsV4 {
|
||||
log.Debugf("Matugen %s detected: using v4 compatibility flags", versionStr)
|
||||
log.Infof("Matugen %s: using v4 flags", versionStr)
|
||||
}
|
||||
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
||||
}
|
||||
@@ -697,7 +677,6 @@ func execDryRun(opts *Options, flags matugenFlags) (string, error) {
|
||||
baseArgs = []string{opts.Kind, opts.Value}
|
||||
}
|
||||
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
||||
baseArgs = appendContrastArg(baseArgs, opts.Contrast)
|
||||
if flags.isV4 {
|
||||
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package matugen
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
||||
@@ -393,51 +392,3 @@ func TestSubstituteVars(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMergedConfigColorsOnly(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
shellDir := filepath.Join(tempDir, "shell")
|
||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||
t.Fatalf("failed to create configs dir: %v", err)
|
||||
}
|
||||
|
||||
baseConfig := "[config]\ncustom_keywords = []\n"
|
||||
if err := os.WriteFile(filepath.Join(configsDir, "base.toml"), []byte(baseConfig), 0o644); err != nil {
|
||||
t.Fatalf("failed to write base config: %v", err)
|
||||
}
|
||||
|
||||
cfgFile, err := os.CreateTemp(tempDir, "merged-*.toml")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp config: %v", err)
|
||||
}
|
||||
defer os.Remove(cfgFile.Name())
|
||||
defer cfgFile.Close()
|
||||
|
||||
opts := &Options{
|
||||
ShellDir: shellDir,
|
||||
ConfigDir: filepath.Join(tempDir, "config"),
|
||||
StateDir: filepath.Join(tempDir, "state"),
|
||||
ColorsOnly: true,
|
||||
}
|
||||
|
||||
if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil {
|
||||
t.Fatalf("buildMergedConfig failed: %v", err)
|
||||
}
|
||||
|
||||
if err := cfgFile.Close(); err != nil {
|
||||
t.Fatalf("failed to close merged config: %v", err)
|
||||
}
|
||||
|
||||
output, err := os.ReadFile(cfgFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read merged config: %v", err)
|
||||
}
|
||||
|
||||
content := string(output)
|
||||
assert.Contains(t, content, "[templates.dank]")
|
||||
assert.Contains(t, content, "output_path = '"+filepath.Join(opts.StateDir, "dms-colors.json")+"'")
|
||||
assert.NotContains(t, content, "[templates.gtk]")
|
||||
assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs")
|
||||
}
|
||||
|
||||
@@ -6,20 +6,12 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/pilebones/go-udev/netlink"
|
||||
)
|
||||
|
||||
const (
|
||||
udevRecvBufSize = 8 * 1024 * 1024
|
||||
udevMaxRetries = 5
|
||||
udevBaseDelay = 2 * time.Second
|
||||
udevMaxDelay = 60 * time.Second
|
||||
)
|
||||
|
||||
type UdevMonitor struct {
|
||||
stop chan struct{}
|
||||
rescanMutex sync.Mutex
|
||||
@@ -37,6 +29,13 @@ func NewUdevMonitor(manager *Manager) *UdevMonitor {
|
||||
}
|
||||
|
||||
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{
|
||||
Rules: []netlink.RuleDefinition{
|
||||
{Env: map[string]string{"SUBSYSTEM": "backlight"}},
|
||||
@@ -49,46 +48,6 @@ func (m *UdevMonitor) run(manager *Manager) {
|
||||
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)
|
||||
errs := make(chan error)
|
||||
conn.Monitor(events, errs, matcher)
|
||||
@@ -98,9 +57,10 @@ func (m *UdevMonitor) monitorLoop(manager *Manager, matcher *netlink.RuleDefinit
|
||||
for {
|
||||
select {
|
||||
case <-m.stop:
|
||||
return nil
|
||||
return
|
||||
case err := <-errs:
|
||||
return err
|
||||
log.Errorf("Udev monitor error: %v", err)
|
||||
return
|
||||
case event := <-events:
|
||||
m.handleEvent(manager, event)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ func handleMatugenQueue(conn net.Conn, req models.Request) {
|
||||
SyncModeWithPortal: models.GetOr(req, "syncModeWithPortal", false),
|
||||
TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false),
|
||||
SkipTemplates: models.GetOr(req, "skipTemplates", ""),
|
||||
Contrast: models.GetOr(req, "contrast", 0.0),
|
||||
}
|
||||
|
||||
wait := models.GetOr(req, "wait", true)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -29,13 +28,7 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
|
||||
|
||||
func TestDetectNetworkStack_Integration(t *testing.T) {
|
||||
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)
|
||||
if assert.NotNil(t, result) {
|
||||
assert.NotEmpty(t, result.ChosenReason)
|
||||
}
|
||||
assert.NotNil(t, result)
|
||||
assert.NotEmpty(t, result.ChosenReason)
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ var dbusManager *serverDbus.Manager
|
||||
var wlContext *wlcontext.SharedContext
|
||||
var themeModeManager *thememode.Manager
|
||||
var locationManager *location.Manager
|
||||
var geoClientInstance geolocation.Client
|
||||
|
||||
const dbusClientID = "dms-dbus-client"
|
||||
|
||||
@@ -192,7 +191,7 @@ func InitializeFreedeskManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeWaylandManager() error {
|
||||
func InitializeWaylandManager(geoClient geolocation.Client) error {
|
||||
log.Info("Attempting to initialize Wayland gamma control...")
|
||||
|
||||
if wlContext == nil {
|
||||
@@ -205,7 +204,7 @@ func InitializeWaylandManager() error {
|
||||
}
|
||||
|
||||
config := wayland.DefaultConfig()
|
||||
manager, err := wayland.NewManager(wlContext.Display(), config)
|
||||
manager, err := wayland.NewManager(wlContext.Display(), geoClient, config)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to initialize wayland manager: %v", err)
|
||||
return err
|
||||
@@ -386,8 +385,8 @@ func InitializeDbusManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeThemeModeManager() error {
|
||||
manager := thememode.NewManager()
|
||||
func InitializeThemeModeManager(geoClient geolocation.Client) error {
|
||||
manager := thememode.NewManager(geoClient)
|
||||
themeModeManager = manager
|
||||
|
||||
log.Info("Theme mode automation manager initialized")
|
||||
@@ -1331,9 +1330,6 @@ func cleanupManagers() {
|
||||
if locationManager != nil {
|
||||
locationManager.Close()
|
||||
}
|
||||
if geoClientInstance != nil {
|
||||
geoClientInstance.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func Start(printDocs bool) error {
|
||||
@@ -1549,6 +1545,9 @@ func Start(printDocs bool) error {
|
||||
loginctlReady := make(chan struct{})
|
||||
freedesktopReady := make(chan struct{})
|
||||
|
||||
geoClient := geolocation.NewClient()
|
||||
defer geoClient.Close()
|
||||
|
||||
go func() {
|
||||
defer close(loginctlReady)
|
||||
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)
|
||||
}
|
||||
|
||||
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() {
|
||||
if err := InitializeBluezManager(); err != nil {
|
||||
log.Warnf("Bluez manager unavailable: %v", err)
|
||||
@@ -1656,6 +1624,25 @@ func Start(printDocs bool) error {
|
||||
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)
|
||||
if wlrOutputManager != nil {
|
||||
go func() {
|
||||
|
||||
@@ -40,7 +40,7 @@ type Manager struct {
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
func NewManager(geoClient geolocation.Client) *Manager {
|
||||
m := &Manager{
|
||||
config: Config{
|
||||
Enabled: false,
|
||||
@@ -54,6 +54,7 @@ func NewManager() *Manager {
|
||||
},
|
||||
stopChan: make(chan struct{}),
|
||||
updateTrigger: make(chan struct{}, 1),
|
||||
geoClient: geoClient,
|
||||
}
|
||||
|
||||
m.updateState(time.Now())
|
||||
@@ -314,10 +315,6 @@ func (m *Manager) getConfig() Config {
|
||||
return m.config
|
||||
}
|
||||
|
||||
func (m *Manager) SetGeoClient(client geolocation.Client) {
|
||||
m.geoClient = client
|
||||
}
|
||||
|
||||
func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
||||
if config.Latitude != nil && config.Longitude != nil {
|
||||
return config.Latitude, config.Longitude
|
||||
@@ -325,9 +322,6 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
||||
if !config.UseIPLocation {
|
||||
return nil, nil
|
||||
}
|
||||
if m.geoClient == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m.locationMutex.RLock()
|
||||
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -41,6 +41,7 @@ func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error
|
||||
updateTrigger: make(chan struct{}, 1),
|
||||
dirty: make(chan struct{}, 1),
|
||||
dbusSignal: make(chan *dbus.Signal, 16),
|
||||
geoClient: geoClient,
|
||||
}
|
||||
|
||||
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) {
|
||||
m.configMutex.RLock()
|
||||
config := m.config
|
||||
@@ -433,31 +430,27 @@ func (m *Manager) getLocation() (*float64, *float64) {
|
||||
if config.Latitude != nil && config.Longitude != nil {
|
||||
return config.Latitude, config.Longitude
|
||||
}
|
||||
if !config.UseIPLocation {
|
||||
return nil, nil
|
||||
}
|
||||
if m.geoClient == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m.locationMutex.RLock()
|
||||
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
||||
lat, lon := m.cachedIPLat, m.cachedIPLon
|
||||
if config.UseIPLocation {
|
||||
m.locationMutex.RLock()
|
||||
if m.cachedIPLat != nil && m.cachedIPLon != nil {
|
||||
lat, lon := m.cachedIPLat, m.cachedIPLon
|
||||
m.locationMutex.RUnlock()
|
||||
return lat, lon
|
||||
}
|
||||
m.locationMutex.RUnlock()
|
||||
return lat, lon
|
||||
}
|
||||
m.locationMutex.RUnlock()
|
||||
|
||||
location, err := m.geoClient.GetLocation()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
location, err := m.geoClient.GetLocation()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m.locationMutex.Lock()
|
||||
m.cachedIPLat = &location.Latitude
|
||||
m.cachedIPLon = &location.Longitude
|
||||
m.locationMutex.Unlock()
|
||||
return m.cachedIPLat, m.cachedIPLon
|
||||
m.locationMutex.Lock()
|
||||
m.cachedIPLat = &location.Latitude
|
||||
m.cachedIPLon = &location.Longitude
|
||||
m.locationMutex.Unlock()
|
||||
return m.cachedIPLat, m.cachedIPLon
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *Manager) hasValidSchedule() bool {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -390,18 +391,20 @@ func TestNotifySubscribers_NonBlocking(t *testing.T) {
|
||||
|
||||
func TestNewManager_GetRegistryError(t *testing.T) {
|
||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
||||
mockGeoclient := mocks_geolocation.NewMockClient(t)
|
||||
|
||||
mockDisplay.EXPECT().Context().Return(nil)
|
||||
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
|
||||
|
||||
config := DefaultConfig()
|
||||
_, err := NewManager(mockDisplay, config)
|
||||
_, err := NewManager(mockDisplay, mockGeoclient, config)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "get registry")
|
||||
}
|
||||
|
||||
func TestNewManager_InvalidConfig(t *testing.T) {
|
||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
||||
mockGeoclient := mocks_geolocation.NewMockClient(t)
|
||||
|
||||
config := Config{
|
||||
LowTemp: 500,
|
||||
@@ -409,6 +412,6 @@ func TestNewManager_InvalidConfig(t *testing.T) {
|
||||
Gamma: 1.0,
|
||||
}
|
||||
|
||||
_, err := NewManager(mockDisplay, config)
|
||||
_, err := NewManager(mockDisplay, mockGeoclient, config)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ package wlcontext
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"golang.org/x/sys/unix"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
@@ -123,9 +123,6 @@ func (sc *SharedContext) eventDispatcher() {
|
||||
{Fd: int32(sc.wakeR), Events: unix.POLLIN},
|
||||
}
|
||||
|
||||
consecutiveErrors := 0
|
||||
const maxConsecutiveErrors = 20
|
||||
|
||||
for {
|
||||
sc.drainCmdQueue()
|
||||
|
||||
@@ -156,19 +153,9 @@ func (sc *SharedContext) eventDispatcher() {
|
||||
}
|
||||
|
||||
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
|
||||
consecutiveErrors++
|
||||
log.Warnf("Wayland connection error (%d/%d): %v", consecutiveErrors, maxConsecutiveErrors, err)
|
||||
|
||||
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
|
||||
log.Errorf("Wayland connection error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
consecutiveErrors = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ func (m Model) viewDeployingConfigs() string {
|
||||
|
||||
spinner := m.spinner.View()
|
||||
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")
|
||||
|
||||
// Show progress information
|
||||
|
||||
@@ -23,7 +23,7 @@ func (m Model) viewDetectingDeps() string {
|
||||
|
||||
spinner := m.spinner.View()
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (m Model) viewInstallingPackages() string {
|
||||
if !m.packageProgress.isComplete {
|
||||
spinner := m.spinner.View()
|
||||
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")
|
||||
|
||||
// Show progress bar
|
||||
@@ -387,7 +387,7 @@ func (m Model) viewDebugLogs() string {
|
||||
|
||||
for i := startIdx; i < len(allLogs); i++ {
|
||||
if allLogs[i] != "" {
|
||||
fmt.Fprintf(&b, "%d: %s\n", i, allLogs[i])
|
||||
b.WriteString(fmt.Sprintf("%d: %s\n", i, allLogs[i]))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ func (m Model) viewFingerprintAuth() string {
|
||||
|
||||
spinner := m.spinner.View()
|
||||
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()
|
||||
|
||||
@@ -132,9 +132,9 @@ func (m Model) viewWelcome() string {
|
||||
contentStyle = contentStyle.Bold(true)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&b, " %s %s\n",
|
||||
b.WriteString(fmt.Sprintf(" %s %s\n",
|
||||
prefixStyle.Render(prefix),
|
||||
contentStyle.Render(content))
|
||||
contentStyle.Render(content)))
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
@@ -158,7 +158,7 @@ func (m Model) viewWelcome() string {
|
||||
} else if m.isLoading {
|
||||
spinner := m.spinner.View()
|
||||
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
|
||||
|
||||
@@ -13,7 +13,7 @@ Architecture: any
|
||||
Depends: ${misc:Depends},
|
||||
greetd,
|
||||
quickshell-git | quickshell
|
||||
Suggests: niri | hyprland | sway
|
||||
Recommends: niri | hyprland | sway
|
||||
Description: DankMaterialShell greeter for greetd
|
||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||
|
||||
@@ -9,7 +9,7 @@ Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
|
||||
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
|
||||
|
||||
Package: dms
|
||||
Architecture: amd64 arm64
|
||||
Architecture: amd64
|
||||
Depends: ${misc:Depends},
|
||||
quickshell | quickshell-git,
|
||||
accountsservice,
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
dms-distropkg-amd64.gz
|
||||
dms-distropkg-arm64.gz
|
||||
dms-source.tar.gz
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# Include files that are normally excluded by .gitignore
|
||||
# These are needed for the build process on Launchpad
|
||||
tar-ignore = !dms-distropkg-amd64.gz
|
||||
tar-ignore = !dms-distropkg-arm64.gz
|
||||
tar-ignore = !dms-source.tar.gz
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
%global debug_package %{nil}
|
||||
%global version {{{ git_repo_version }}}
|
||||
%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
|
||||
Epoch: 2
|
||||
|
||||
@@ -24,7 +24,6 @@ let
|
||||
lib.makeBinPath [
|
||||
cfg.quickshell.package
|
||||
compositorPackage
|
||||
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
|
||||
]
|
||||
}
|
||||
${
|
||||
@@ -196,9 +195,7 @@ in
|
||||
fi
|
||||
|
||||
if [ -f settings.json ]; then
|
||||
theme_file="$(${jq} -r '.customThemeFile // empty' settings.json)"
|
||||
if [ -f "$theme_file" ] && [ -r "$theme_file" ]; then
|
||||
cp "$theme_file" custom-theme.json
|
||||
if cp "$(${jq} -r '.customThemeFile' settings.json)" custom-theme.json; then
|
||||
mv settings.json settings.orig.json
|
||||
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
|
||||
fi
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
}:
|
||||
let
|
||||
cfg = config.programs.dank-material-shell;
|
||||
in {
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
./dms-rename.nix
|
||||
];
|
||||
@@ -14,11 +16,9 @@ in {
|
||||
enableKeybinds = lib.mkEnableOption "DankMaterialShell niri keybinds";
|
||||
enableSpawn = lib.mkEnableOption "DankMaterialShell niri spawn-at-startup";
|
||||
includes = {
|
||||
enable =
|
||||
(lib.mkEnableOption "includes for niri-flake")
|
||||
// {
|
||||
default = true;
|
||||
};
|
||||
enable = (lib.mkEnableOption "includes for niri-flake") // {
|
||||
default = true;
|
||||
};
|
||||
override = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
description = ''
|
||||
@@ -44,10 +44,8 @@ in {
|
||||
"alttab"
|
||||
"binds"
|
||||
"colors"
|
||||
"cursor"
|
||||
"layout"
|
||||
"outputs"
|
||||
"windowrules"
|
||||
"wpblur"
|
||||
];
|
||||
example = [
|
||||
@@ -72,21 +70,24 @@ in {
|
||||
let
|
||||
cfg' = cfg.niri.includes;
|
||||
|
||||
withOriginalConfig = dmsFiles:
|
||||
if cfg'.override
|
||||
then [cfg'.originalFileName] ++ dmsFiles
|
||||
else dmsFiles ++ [cfg'.originalFileName];
|
||||
withOriginalConfig =
|
||||
dmsFiles:
|
||||
if cfg'.override then
|
||||
[ cfg'.originalFileName ] ++ dmsFiles
|
||||
else
|
||||
dmsFiles ++ [ cfg'.originalFileName ];
|
||||
|
||||
fixes = map (fix: "\n${fix}") (
|
||||
lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable)
|
||||
# kdl
|
||||
''
|
||||
// Border fix
|
||||
// See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details
|
||||
layout { border { on; }; }
|
||||
''
|
||||
# kdl
|
||||
''
|
||||
// Border fix
|
||||
// See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details
|
||||
layout { border { on; }; }
|
||||
''
|
||||
);
|
||||
in {
|
||||
in
|
||||
{
|
||||
niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl";
|
||||
niri-config-dms = {
|
||||
target = "niri/config.kdl";
|
||||
@@ -103,9 +104,11 @@ in {
|
||||
|
||||
programs.niri.settings = lib.mkMerge [
|
||||
(lib.mkIf cfg.niri.enableKeybinds {
|
||||
binds = with config.lib.niri.actions; let
|
||||
dms-ipc = spawn "dms" "ipc";
|
||||
in
|
||||
binds =
|
||||
with config.lib.niri.actions;
|
||||
let
|
||||
dms-ipc = spawn "dms" "ipc";
|
||||
in
|
||||
{
|
||||
"Mod+Space" = {
|
||||
action = dms-ipc "spotlight" "toggle";
|
||||
|
||||
@@ -102,19 +102,6 @@ if [[ ! -d "distro/debian" ]]; then
|
||||
echo "Error: Run this script from the repository root"
|
||||
exit 1
|
||||
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:
|
||||
# $1 = PROJECT
|
||||
# $2 = PACKAGE
|
||||
@@ -322,23 +309,8 @@ mkdir -p "$OBS_BASE"
|
||||
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
|
||||
echo "Checking out $OBS_PROJECT/$PACKAGE..."
|
||||
cd "$OBS_BASE"
|
||||
CHECKOUT_OK=false
|
||||
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
|
||||
osc co "$OBS_PROJECT/$PACKAGE"
|
||||
cd "$REPO_ROOT"
|
||||
if [[ "$CHECKOUT_OK" != "true" ]]; then
|
||||
echo "Error: Checkout failed after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
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)
|
||||
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"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1173,7 +1145,7 @@ if ! osc status 2>/dev/null | grep -qE '^[MAD]|^[?]'; then
|
||||
else
|
||||
echo "==> Committing to OBS"
|
||||
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]}
|
||||
set -e
|
||||
if [[ $COMMIT_EXIT -ne 0 ]]; then
|
||||
|
||||
@@ -13,7 +13,7 @@ Architecture: any
|
||||
Depends: ${misc:Depends},
|
||||
greetd,
|
||||
quickshell-git | quickshell
|
||||
Suggests: niri | hyprland | sway
|
||||
Recommends: niri | hyprland | sway
|
||||
Description: DankMaterialShell greeter for greetd
|
||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||
|
||||
@@ -538,8 +538,6 @@ Color picker modal control.
|
||||
|
||||
**Functions:**
|
||||
- `open` - Show color picker modal
|
||||
- `openColor <color>` - Show color picker modal with a pre-selected color
|
||||
- Parameters: `color` - Color string (e.g. "#ff0000", "#3f51b5")
|
||||
- `close` - Hide color picker modal
|
||||
- `closeInstant` - Hide color picker modal without animation
|
||||
- `toggle` - Toggle color picker modal visibility
|
||||
|
||||
139
flake.nix
139
flake.nix
@@ -17,25 +17,6 @@
|
||||
...
|
||||
}:
|
||||
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 =
|
||||
fn:
|
||||
nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] (
|
||||
@@ -91,82 +72,76 @@
|
||||
"${cleanVersion}${dateSuffix}${revSuffix}";
|
||||
in
|
||||
{
|
||||
dms-shell = pkgs.lib.makeOverridable (
|
||||
dms-shell = pkgs.buildGoModule (
|
||||
let
|
||||
rootSrc = ./.;
|
||||
in
|
||||
{
|
||||
extraQtPackages ? [ ],
|
||||
}:
|
||||
(pkgs.buildGoModule.override { go = goForPkgs pkgs; }) (
|
||||
let
|
||||
rootSrc = ./.;
|
||||
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
|
||||
in
|
||||
{
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
|
||||
|
||||
subPackages = [ "cmd/dms" ];
|
||||
subPackages = [ "cmd/dms" ];
|
||||
|
||||
ldflags = [
|
||||
"-s"
|
||||
"-w"
|
||||
"-X 'main.Version=${version}'"
|
||||
];
|
||||
ldflags = [
|
||||
"-s"
|
||||
"-w"
|
||||
"-X 'main.Version=${version}'"
|
||||
];
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
installShellFiles
|
||||
makeWrapper
|
||||
];
|
||||
nativeBuildInputs = with pkgs; [
|
||||
installShellFiles
|
||||
makeWrapper
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
mkdir -p $out/share/quickshell/dms
|
||||
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
||||
postInstall = ''
|
||||
mkdir -p $out/share/quickshell/dms
|
||||
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
||||
|
||||
chmod u+w $out/share/quickshell/dms/VERSION
|
||||
echo "${version}" > $out/share/quickshell/dms/VERSION
|
||||
chmod u+w $out/share/quickshell/dms/VERSION
|
||||
echo "${version}" > $out/share/quickshell/dms/VERSION
|
||||
|
||||
# Install desktop file and icon
|
||||
install -D ${rootSrc}/assets/dms-open.desktop \
|
||||
$out/share/applications/dms-open.desktop
|
||||
install -D ${rootSrc}/core/assets/danklogo.svg \
|
||||
$out/share/hicolor/scalable/apps/danklogo.svg
|
||||
# Install desktop file and icon
|
||||
install -D ${rootSrc}/assets/dms-open.desktop \
|
||||
$out/share/applications/dms-open.desktop
|
||||
install -D ${rootSrc}/core/assets/danklogo.svg \
|
||||
$out/share/hicolor/scalable/apps/danklogo.svg
|
||||
|
||||
wrapProgram $out/bin/dms \
|
||||
--add-flags "-c $out/share/quickshell/dms" \
|
||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
|
||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
|
||||
wrapProgram $out/bin/dms \
|
||||
--add-flags "-c $out/share/quickshell/dms" \
|
||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}" \
|
||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs (qmlPkgs pkgs)}"
|
||||
|
||||
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
||||
$out/lib/systemd/user/dms.service
|
||||
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
||||
$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/pkill ${pkgs.procps}/bin/pkill
|
||||
substituteInPlace $out/lib/systemd/user/dms.service \
|
||||
--replace-fail /usr/bin/dms $out/bin/dms \
|
||||
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
||||
|
||||
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
||||
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
||||
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
||||
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
||||
|
||||
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
||||
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
||||
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
||||
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
||||
|
||||
installShellCompletion --cmd dms \
|
||||
--bash <($out/bin/dms completion bash) \
|
||||
--fish <($out/bin/dms completion fish) \
|
||||
--zsh <($out/bin/dms completion zsh)
|
||||
'';
|
||||
installShellCompletion --cmd dms \
|
||||
--bash <($out/bin/dms completion bash) \
|
||||
--fish <($out/bin/dms completion fish) \
|
||||
--zsh <($out/bin/dms completion zsh)
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
||||
homepage = "https://danklinux.com";
|
||||
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
||||
license = pkgs.lib.licenses.mit;
|
||||
mainProgram = "dms";
|
||||
platforms = pkgs.lib.platforms.linux;
|
||||
};
|
||||
}
|
||||
)
|
||||
) { };
|
||||
meta = {
|
||||
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
||||
homepage = "https://danklinux.com";
|
||||
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
||||
license = pkgs.lib.licenses.mit;
|
||||
mainProgram = "dms";
|
||||
platforms = pkgs.lib.platforms.linux;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
quickshell = quickshell.packages.${system}.default;
|
||||
|
||||
@@ -206,7 +181,7 @@
|
||||
buildInputs =
|
||||
with pkgs;
|
||||
[
|
||||
(goForPkgs pkgs)
|
||||
go_1_25
|
||||
go-mockery_2
|
||||
gopls
|
||||
delve
|
||||
|
||||
@@ -99,7 +99,7 @@ qs -v -p shell.qml # Verbose debugging
|
||||
|
||||
# Code formatting and linting
|
||||
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
|
||||
```
|
||||
|
||||
@@ -783,7 +783,7 @@ When modifying the shell:
|
||||
|
||||
**QML Frontend:**
|
||||
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)
|
||||
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
|
||||
|
||||
|
||||
174
quickshell/Common/AnimVariants.qml
Normal file
174
quickshell/Common/AnimVariants.qml
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,4 +22,9 @@ Singleton {
|
||||
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 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]
|
||||
}
|
||||
|
||||
@@ -72,8 +72,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function resolveIconPath(iconName: string): string {
|
||||
if (!iconName)
|
||||
return "";
|
||||
if (!iconName) return "";
|
||||
const moddedId = moddedAppId(iconName);
|
||||
if (moddedId !== iconName) {
|
||||
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
||||
@@ -86,8 +85,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function resolveIconUrl(iconName: string): string {
|
||||
if (!iconName)
|
||||
return "";
|
||||
if (!iconName) return "";
|
||||
const moddedId = moddedAppId(iconName);
|
||||
if (moddedId !== iconName) {
|
||||
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
||||
@@ -100,8 +98,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function getAppIcon(appId: string, desktopEntry: var): string {
|
||||
// ! TODO - after QS 0.3, we can install our icon properly
|
||||
if (appId === "org.quickshell" || appId === "com.danklinux.dms") {
|
||||
if (appId === "org.quickshell") {
|
||||
return Qt.resolvedUrl("../assets/danklogo.svg");
|
||||
}
|
||||
|
||||
@@ -121,7 +118,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function getAppName(appId: string, desktopEntry: var): string {
|
||||
if (appId === "org.quickshell" || appId === "com.danklinux.dms") {
|
||||
if (appId === "org.quickshell") {
|
||||
return "dms";
|
||||
}
|
||||
|
||||
|
||||
@@ -12,27 +12,6 @@ Singleton {
|
||||
signal popoutOpening
|
||||
signal popoutChanged
|
||||
|
||||
function _closePopout(popout) {
|
||||
switch (true) {
|
||||
case popout.dashVisible !== undefined:
|
||||
popout.dashVisible = false;
|
||||
return;
|
||||
case popout.notificationHistoryVisible !== undefined:
|
||||
popout.notificationHistoryVisible = false;
|
||||
return;
|
||||
default:
|
||||
popout.close();
|
||||
}
|
||||
}
|
||||
|
||||
function _isStale(popout) {
|
||||
try {
|
||||
return !popout || !("shouldBeVisible" in popout);
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function showPopout(popout) {
|
||||
if (!popout || !popout.screen)
|
||||
return;
|
||||
@@ -44,11 +23,13 @@ Singleton {
|
||||
const otherPopout = currentPopoutsByScreen[otherScreenName];
|
||||
if (!otherPopout || otherPopout === popout)
|
||||
continue;
|
||||
if (_isStale(otherPopout)) {
|
||||
currentPopoutsByScreen[otherScreenName] = null;
|
||||
continue;
|
||||
if (otherPopout.dashVisible !== undefined) {
|
||||
otherPopout.dashVisible = false;
|
||||
} else if (otherPopout.notificationHistoryVisible !== undefined) {
|
||||
otherPopout.notificationHistoryVisible = false;
|
||||
} else {
|
||||
otherPopout.close();
|
||||
}
|
||||
_closePopout(otherPopout);
|
||||
}
|
||||
|
||||
currentPopoutsByScreen[screenName] = popout;
|
||||
@@ -70,9 +51,15 @@ Singleton {
|
||||
function closeAllPopouts() {
|
||||
for (const screenName in currentPopoutsByScreen) {
|
||||
const popout = currentPopoutsByScreen[screenName];
|
||||
if (!popout || _isStale(popout))
|
||||
if (!popout)
|
||||
continue;
|
||||
_closePopout(popout);
|
||||
if (popout.dashVisible !== undefined) {
|
||||
popout.dashVisible = false;
|
||||
} else if (popout.notificationHistoryVisible !== undefined) {
|
||||
popout.notificationHistoryVisible = false;
|
||||
} else {
|
||||
popout.close();
|
||||
}
|
||||
}
|
||||
currentPopoutsByScreen = {};
|
||||
}
|
||||
@@ -103,12 +90,6 @@ Singleton {
|
||||
if (!otherPopout)
|
||||
continue;
|
||||
|
||||
if (_isStale(otherPopout)) {
|
||||
currentPopoutsByScreen[otherScreenName] = null;
|
||||
currentPopoutTriggers[otherScreenName] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (otherPopout === popout) {
|
||||
movedFromOtherScreen = true;
|
||||
currentPopoutsByScreen[otherScreenName] = null;
|
||||
@@ -116,26 +97,45 @@ Singleton {
|
||||
continue;
|
||||
}
|
||||
|
||||
_closePopout(otherPopout);
|
||||
if (otherPopout.dashVisible !== undefined) {
|
||||
otherPopout.dashVisible = false;
|
||||
} else if (otherPopout.notificationHistoryVisible !== undefined) {
|
||||
otherPopout.notificationHistoryVisible = false;
|
||||
} else {
|
||||
otherPopout.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPopout && currentPopout !== popout) {
|
||||
if (_isStale(currentPopout)) {
|
||||
currentPopoutsByScreen[screenName] = null;
|
||||
currentPopoutTriggers[screenName] = null;
|
||||
if (currentPopout.dashVisible !== undefined) {
|
||||
currentPopout.dashVisible = false;
|
||||
} else if (currentPopout.notificationHistoryVisible !== undefined) {
|
||||
currentPopout.notificationHistoryVisible = false;
|
||||
} else {
|
||||
_closePopout(currentPopout);
|
||||
currentPopout.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
|
||||
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
|
||||
_closePopout(popout);
|
||||
if (popout.dashVisible !== undefined) {
|
||||
popout.dashVisible = false;
|
||||
} else if (popout.notificationHistoryVisible !== undefined) {
|
||||
popout.notificationHistoryVisible = false;
|
||||
} else {
|
||||
popout.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (triggerId === undefined) {
|
||||
_closePopout(popout);
|
||||
if (popout.dashVisible !== undefined) {
|
||||
popout.dashVisible = false;
|
||||
} else if (popout.notificationHistoryVisible !== undefined) {
|
||||
popout.notificationHistoryVisible = false;
|
||||
} else {
|
||||
popout.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ Singleton {
|
||||
property bool _hasUnsavedChanges: false
|
||||
property var _loadedSessionSnapshot: null
|
||||
readonly property var _hooks: ({
|
||||
"updateLocale": updateLocale
|
||||
})
|
||||
"updateLocale": updateLocale
|
||||
})
|
||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||
|
||||
@@ -134,8 +134,6 @@ Singleton {
|
||||
property string launcherLastMode: "all"
|
||||
property string appDrawerLastMode: "apps"
|
||||
property string niriOverviewLastMode: "apps"
|
||||
property string settingsSidebarExpandedIds: ","
|
||||
property string settingsSidebarCollapsedIds: ","
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!isGreeterMode) {
|
||||
@@ -582,7 +580,14 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
newSettings[identifier] = getMonitorCyclingSettings(screenName);
|
||||
if (!newSettings[identifier]) {
|
||||
newSettings[identifier] = {
|
||||
"enabled": false,
|
||||
"mode": "interval",
|
||||
"interval": 300,
|
||||
"time": "06:00"
|
||||
};
|
||||
}
|
||||
newSettings[identifier].enabled = enabled;
|
||||
monitorCyclingSettings = newSettings;
|
||||
saveSettings();
|
||||
@@ -613,7 +618,14 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
newSettings[identifier] = getMonitorCyclingSettings(screenName);
|
||||
if (!newSettings[identifier]) {
|
||||
newSettings[identifier] = {
|
||||
"enabled": false,
|
||||
"mode": "interval",
|
||||
"interval": 300,
|
||||
"time": "06:00"
|
||||
};
|
||||
}
|
||||
newSettings[identifier].mode = mode;
|
||||
monitorCyclingSettings = newSettings;
|
||||
saveSettings();
|
||||
@@ -644,7 +656,14 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
newSettings[identifier] = getMonitorCyclingSettings(screenName);
|
||||
if (!newSettings[identifier]) {
|
||||
newSettings[identifier] = {
|
||||
"enabled": false,
|
||||
"mode": "interval",
|
||||
"interval": 300,
|
||||
"time": "06:00"
|
||||
};
|
||||
}
|
||||
newSettings[identifier].interval = interval;
|
||||
monitorCyclingSettings = newSettings;
|
||||
saveSettings();
|
||||
@@ -675,7 +694,14 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
newSettings[identifier] = getMonitorCyclingSettings(screenName);
|
||||
if (!newSettings[identifier]) {
|
||||
newSettings[identifier] = {
|
||||
"enabled": false,
|
||||
"mode": "interval",
|
||||
"interval": 300,
|
||||
"time": "06:00"
|
||||
};
|
||||
}
|
||||
newSettings[identifier].time = time;
|
||||
monitorCyclingSettings = newSettings;
|
||||
saveSettings();
|
||||
@@ -1106,12 +1132,6 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setSettingsSidebarState(expandedIds, collapsedIds) {
|
||||
settingsSidebarExpandedIds = expandedIds;
|
||||
settingsSidebarCollapsedIds = collapsedIds;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function syncWallpaperForCurrentMode() {
|
||||
if (!perModeWallpaper)
|
||||
return;
|
||||
@@ -1198,7 +1218,7 @@ Singleton {
|
||||
"time": "06:00"
|
||||
};
|
||||
var value = _findMonitorValue(monitorCyclingSettings, screenName);
|
||||
return Object.assign({}, defaults, value !== undefined ? value : {});
|
||||
return value !== undefined ? value : defaults;
|
||||
}
|
||||
|
||||
FileView {
|
||||
@@ -1225,7 +1245,7 @@ Singleton {
|
||||
id: greeterSessionFile
|
||||
|
||||
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";
|
||||
}
|
||||
preload: isGreeterMode
|
||||
|
||||
@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
|
||||
Singleton {
|
||||
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"
|
||||
|
||||
@@ -37,6 +37,18 @@ Singleton {
|
||||
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 {
|
||||
Suspend,
|
||||
Hibernate,
|
||||
@@ -130,7 +142,6 @@ Singleton {
|
||||
property string customThemeFile: ""
|
||||
property var registryThemeVariants: ({})
|
||||
property string matugenScheme: "scheme-tonal-spot"
|
||||
property real matugenContrast: 0
|
||||
property bool runUserMatugenTemplates: true
|
||||
property string matugenTargetMonitor: ""
|
||||
property real popupTransparency: 1.0
|
||||
@@ -151,7 +162,6 @@ Singleton {
|
||||
property int mangoLayoutBorderSize: -1
|
||||
|
||||
property int firstDayOfWeek: -1
|
||||
property bool showWeekNumber: false
|
||||
property bool use24HourClock: true
|
||||
property bool showSeconds: false
|
||||
property bool padHours12Hour: false
|
||||
@@ -168,6 +178,12 @@ Singleton {
|
||||
property int modalCustomAnimationDuration: 150
|
||||
property bool enableRippleEffects: true
|
||||
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
|
||||
onM3ElevationEnabledChanged: saveSettings()
|
||||
property int m3ElevationIntensity: 12
|
||||
@@ -282,7 +298,6 @@ Singleton {
|
||||
property bool showOccupiedWorkspacesOnly: false
|
||||
property bool reverseScrolling: false
|
||||
property bool dwlShowAllTags: false
|
||||
property bool workspaceActiveAppHighlightEnabled: false
|
||||
property string workspaceColorMode: "default"
|
||||
property string workspaceOccupiedColorMode: "none"
|
||||
property string workspaceUnfocusedColorMode: "default"
|
||||
@@ -316,17 +331,6 @@ Singleton {
|
||||
property string centeringMode: "index"
|
||||
property string clockDateFormat: ""
|
||||
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 string appLauncherViewMode: "list"
|
||||
@@ -455,11 +459,6 @@ Singleton {
|
||||
property bool syncModeWithPortal: true
|
||||
property bool terminalsAlwaysDark: false
|
||||
|
||||
property string muxType: "tmux"
|
||||
property bool muxUseCustomCommand: false
|
||||
property string muxCustomCommand: ""
|
||||
property string muxSessionFilter: ""
|
||||
|
||||
property bool runDmsMatugenTemplates: true
|
||||
property bool matugenTemplateGtk: true
|
||||
property bool matugenTemplateNiri: true
|
||||
@@ -475,31 +474,18 @@ Singleton {
|
||||
property bool matugenTemplateGhostty: true
|
||||
property bool matugenTemplateKitty: true
|
||||
property bool matugenTemplateFoot: true
|
||||
property bool matugenTemplateNeovim: false
|
||||
property bool matugenTemplateNeovim: true
|
||||
property bool matugenTemplateAlacritty: true
|
||||
property bool matugenTemplateWezterm: true
|
||||
property bool matugenTemplateDgop: true
|
||||
property bool matugenTemplateKcolorscheme: true
|
||||
property bool matugenTemplateVscode: 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 dockAutoHide: false
|
||||
property bool dockSmartAutoHide: false
|
||||
property bool dockGroupByApp: false
|
||||
property bool dockRestoreSpecialWorkspaceOnClick: false
|
||||
property bool dockOpenOnOverview: false
|
||||
property int dockPosition: SettingsData.Position.Bottom
|
||||
property real dockSpacing: 4
|
||||
@@ -545,23 +531,9 @@ Singleton {
|
||||
property bool enableFprint: false
|
||||
property int maxFprintTries: 15
|
||||
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 string u2fMode: "or"
|
||||
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 lockScreenInactiveColor: "#000000"
|
||||
property int lockScreenNotificationMode: 0
|
||||
@@ -584,7 +556,6 @@ Singleton {
|
||||
property bool notificationHistorySaveNormal: true
|
||||
property bool notificationHistorySaveCritical: true
|
||||
property var notificationRules: []
|
||||
property bool notificationFocusedMonitor: false
|
||||
|
||||
property bool osdAlwaysShowValue: false
|
||||
property int osdPosition: SettingsData.Position.BottomCenter
|
||||
@@ -1048,19 +1019,13 @@ Singleton {
|
||||
signal widgetDataChanged
|
||||
signal workspaceIconsUpdated
|
||||
|
||||
function refreshAuthAvailability() {
|
||||
if (isGreeterMode)
|
||||
return;
|
||||
Processes.settingsRoot = root;
|
||||
Processes.detectAuthCapabilities();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!isGreeterMode) {
|
||||
Processes.settingsRoot = root;
|
||||
loadSettings();
|
||||
initializeListModels();
|
||||
refreshAuthAvailability();
|
||||
Processes.detectFprintd();
|
||||
Processes.detectU2f();
|
||||
Processes.checkPluginSettings();
|
||||
}
|
||||
}
|
||||
@@ -1208,7 +1173,7 @@ Singleton {
|
||||
"updateCompositorLayout": updateCompositorLayout,
|
||||
"applyStoredIconTheme": applyStoredIconTheme,
|
||||
"updateBarConfigs": updateBarConfigs,
|
||||
"updateCompositorCursor": updateCompositorCursor
|
||||
"updateCompositorCursor": updateCompositorCursor,
|
||||
})
|
||||
|
||||
function set(key, value) {
|
||||
@@ -1300,45 +1265,10 @@ Singleton {
|
||||
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() {
|
||||
try {
|
||||
parsePluginSettings(pluginSettingsFile.text());
|
||||
} catch (e) {
|
||||
const msg = e.message || String(e);
|
||||
if (!_isMissingPluginSettingsError(e))
|
||||
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
|
||||
_resetPluginSettings();
|
||||
}
|
||||
_pluginSettingsLoading = true;
|
||||
parsePluginSettings(pluginSettingsFile.text());
|
||||
_pluginSettingsLoading = false;
|
||||
}
|
||||
|
||||
function parsePluginSettings(content) {
|
||||
@@ -1947,12 +1877,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function setMatugenContrast(value) {
|
||||
if (matugenContrast === value)
|
||||
return;
|
||||
set("matugenContrast", value);
|
||||
}
|
||||
|
||||
function setRunUserMatugenTemplates(enabled) {
|
||||
if (runUserMatugenTemplates === enabled)
|
||||
return;
|
||||
@@ -2780,7 +2704,6 @@ Singleton {
|
||||
blockLoading: true
|
||||
blockWrites: true
|
||||
atomicWrites: true
|
||||
printErrors: false
|
||||
watchChanges: !isGreeterMode
|
||||
onLoaded: {
|
||||
if (!isGreeterMode) {
|
||||
@@ -2789,10 +2712,7 @@ Singleton {
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
if (!isGreeterMode) {
|
||||
const msg = String(error || "");
|
||||
if (!_isMissingPluginSettingsError(error))
|
||||
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
|
||||
_resetPluginSettings();
|
||||
pluginSettings = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -960,6 +960,24 @@ Singleton {
|
||||
"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: {
|
||||
"none": 0,
|
||||
"short": 250,
|
||||
@@ -1084,7 +1102,7 @@ Singleton {
|
||||
|
||||
property string fontFamily: {
|
||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
||||
return GreetdSettings.getEffectiveFontFamily();
|
||||
return GreetdSettings.fontFamily;
|
||||
}
|
||||
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
||||
}
|
||||
@@ -1248,8 +1266,7 @@ Singleton {
|
||||
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
|
||||
const defaults = themeData.variants.defaults || {};
|
||||
const modeDefaults = defaults[colorMode] || defaults.dark || {};
|
||||
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||
const stored = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.[colorMode] || modeDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults);
|
||||
const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults;
|
||||
var flavorId = stored.flavor || modeDefaults.flavor || "";
|
||||
const accentId = stored.accent || modeDefaults.accent || "";
|
||||
var flavor = findVariant(themeData.variants.flavors, flavorId);
|
||||
@@ -1275,8 +1292,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (themeData.variants.options && themeData.variants.options.length > 0) {
|
||||
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||
const selectedVariantId = isGreeterMode ? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : themeData.variants.default) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default);
|
||||
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default;
|
||||
const variant = findVariant(themeData.variants.options, selectedVariantId);
|
||||
if (variant) {
|
||||
const variantColors = variant[colorMode] || variant.dark || variant.light || {};
|
||||
@@ -1549,14 +1565,11 @@ Singleton {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.terminalsAlwaysDark) {
|
||||
args.push("--terminals-always-dark");
|
||||
}
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.matugenContrast !== 0) {
|
||||
args.push("--contrast", SettingsData.matugenContrast.toString());
|
||||
}
|
||||
|
||||
if (typeof SettingsData !== "undefined") {
|
||||
const skipTemplates = [];
|
||||
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 {
|
||||
if (!SettingsData.matugenTemplateGtk)
|
||||
skipTemplates.push("gtk");
|
||||
@@ -1600,8 +1613,6 @@ Singleton {
|
||||
skipTemplates.push("vscode");
|
||||
if (!SettingsData.matugenTemplateEmacs)
|
||||
skipTemplates.push("emacs");
|
||||
if (!SettingsData.matugenTemplateZed)
|
||||
skipTemplates.push("zed");
|
||||
}
|
||||
if (skipTemplates.length > 0) {
|
||||
args.push("--skip-templates", skipTemplates.join(","));
|
||||
@@ -1651,9 +1662,8 @@ Singleton {
|
||||
const defaults = customThemeRawData.variants.defaults || {};
|
||||
const darkDefaults = defaults.dark || {};
|
||||
const lightDefaults = defaults.light || defaults.dark || {};
|
||||
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||
const storedDark = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.dark || darkDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults);
|
||||
const storedLight = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.light || lightDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults);
|
||||
const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults;
|
||||
const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults;
|
||||
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
|
||||
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
|
||||
const accentId = storedDark.accent || darkDefaults.accent || "";
|
||||
@@ -1671,8 +1681,7 @@ Singleton {
|
||||
lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {});
|
||||
}
|
||||
} else if (customThemeRawData.variants.options) {
|
||||
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||
const selectedVariantId = isGreeterMode ? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : customThemeRawData.variants.default) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default);
|
||||
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default;
|
||||
const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
|
||||
if (variant) {
|
||||
darkTheme = mergeColors(darkTheme, variant.dark || {});
|
||||
@@ -1996,11 +2005,10 @@ Singleton {
|
||||
FileView {
|
||||
id: dynamicColorsFileView
|
||||
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";
|
||||
return colorsPath;
|
||||
}
|
||||
blockLoading: false
|
||||
watchChanges: !SessionData.isGreeterMode
|
||||
|
||||
function parseAndLoadColors() {
|
||||
|
||||
@@ -10,352 +10,22 @@ Singleton {
|
||||
|
||||
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() {
|
||||
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() {
|
||||
detectAuthCapabilities();
|
||||
fprintdDetectionProcess.running = true;
|
||||
}
|
||||
|
||||
function detectU2f() {
|
||||
detectAuthCapabilities();
|
||||
u2fDetectionProcess.running = true;
|
||||
}
|
||||
|
||||
function checkPluginSettings() {
|
||||
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 {
|
||||
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
|
||||
@@ -365,15 +35,15 @@ Singleton {
|
||||
if (!settingsRoot)
|
||||
return;
|
||||
if (text && text.trim()) {
|
||||
const lines = text.trim().split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.startsWith("qt5ct:")) {
|
||||
settingsRoot.qt5ctAvailable = line.split(":")[1] === "true";
|
||||
} else if (line.startsWith("qt6ct:")) {
|
||||
settingsRoot.qt6ctAvailable = line.split(":")[1] === "true";
|
||||
} else if (line.startsWith("gtk:")) {
|
||||
settingsRoot.gtkAvailable = line.split(":")[1] === "true";
|
||||
var lines = text.trim().split('\n');
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i];
|
||||
if (line.startsWith('qt5ct:')) {
|
||||
settingsRoot.qt5ctAvailable = line.split(':')[1] === 'true';
|
||||
} else if (line.startsWith('qt6ct:')) {
|
||||
settingsRoot.qt6ctAvailable = line.split(':')[1] === 'true';
|
||||
} else if (line.startsWith('gtk:')) {
|
||||
settingsRoot.gtkAvailable = line.split(':')[1] === 'true';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,181 +51,23 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
property var fingerprintProbeProcess: 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"]
|
||||
property var fprintdDetectionProcess: Process {
|
||||
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.fingerprintProbeOutput = text || "";
|
||||
root.fingerprintProbeStreamFinished = true;
|
||||
root.finalizeFingerprintProbe();
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function (exitCode) {
|
||||
root.fingerprintProbeExitCode = exitCode;
|
||||
root.fingerprintProbeExited = true;
|
||||
root.finalizeFingerprintProbe();
|
||||
if (!settingsRoot)
|
||||
return;
|
||||
settingsRoot.fprintdAvailable = (exitCode === 0);
|
||||
}
|
||||
}
|
||||
|
||||
property var pamSupportDetectionProcess: 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"]
|
||||
property var u2fDetectionProcess: Process {
|
||||
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
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.pamSupportProbeOutput = text || "";
|
||||
root.pamSupportProbeStreamFinished = true;
|
||||
root.finalizePamSupportProbe();
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function (exitCode) {
|
||||
root.pamSupportProbeExitCode = exitCode;
|
||||
root.pamSupportProbeExited = true;
|
||||
root.finalizePamSupportProbe();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
if (!settingsRoot)
|
||||
return;
|
||||
settingsRoot.u2fAvailable = (exitCode === 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,10 +84,7 @@ var SPEC = {
|
||||
|
||||
launcherLastMode: { def: "all" },
|
||||
appDrawerLastMode: { def: "apps" },
|
||||
niriOverviewLastMode: { def: "apps" },
|
||||
|
||||
settingsSidebarExpandedIds: { def: "," },
|
||||
settingsSidebarCollapsedIds: { def: "," }
|
||||
niriOverviewLastMode: { def: "apps" }
|
||||
};
|
||||
|
||||
function getValidKeys() {
|
||||
|
||||
@@ -11,7 +11,6 @@ var SPEC = {
|
||||
customThemeFile: { def: "" },
|
||||
registryThemeVariants: { def: {} },
|
||||
matugenScheme: { def: "scheme-tonal-spot", onChange: "regenSystemThemes" },
|
||||
matugenContrast: { def: 0, onChange: "regenSystemThemes" },
|
||||
runUserMatugenTemplates: { def: true, onChange: "regenSystemThemes" },
|
||||
matugenTargetMonitor: { def: "", onChange: "regenSystemThemes" },
|
||||
|
||||
@@ -34,7 +33,6 @@ var SPEC = {
|
||||
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||
|
||||
firstDayOfWeek: { def: -1 },
|
||||
showWeekNumber: { def: false },
|
||||
use24HourClock: { def: true },
|
||||
showSeconds: { def: false },
|
||||
padHours12Hour: { def: false },
|
||||
@@ -49,6 +47,9 @@ var SPEC = {
|
||||
modalAnimationSpeed: { def: 1 },
|
||||
modalCustomAnimationDuration: { def: 150 },
|
||||
enableRippleEffects: { def: true },
|
||||
animationVariant: { def: 0 },
|
||||
motionEffect: { def: 0 },
|
||||
directionalAnimationMode: { def: 0 },
|
||||
m3ElevationEnabled: { def: true },
|
||||
m3ElevationIntensity: { def: 12 },
|
||||
m3ElevationOpacity: { def: 30 },
|
||||
@@ -125,7 +126,6 @@ var SPEC = {
|
||||
showOccupiedWorkspacesOnly: { def: false },
|
||||
reverseScrolling: { def: false },
|
||||
dwlShowAllTags: { def: false },
|
||||
workspaceActiveAppHighlightEnabled: { def: false },
|
||||
workspaceColorMode: { def: "default" },
|
||||
workspaceOccupiedColorMode: { def: "none" },
|
||||
workspaceUnfocusedColorMode: { def: "default" },
|
||||
@@ -167,17 +167,6 @@ var SPEC = {
|
||||
centeringMode: { def: "index" },
|
||||
clockDateFormat: { 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 },
|
||||
|
||||
appLauncherViewMode: { def: "list" },
|
||||
@@ -270,11 +259,6 @@ var SPEC = {
|
||||
syncModeWithPortal: { def: true },
|
||||
terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" },
|
||||
|
||||
muxType: { def: "tmux" },
|
||||
muxUseCustomCommand: { def: false },
|
||||
muxCustomCommand: { def: "" },
|
||||
muxSessionFilter: { def: "" },
|
||||
|
||||
runDmsMatugenTemplates: { def: true },
|
||||
matugenTemplateGtk: { def: true },
|
||||
matugenTemplateNiri: { def: true },
|
||||
@@ -291,26 +275,17 @@ var SPEC = {
|
||||
matugenTemplateKitty: { def: true },
|
||||
matugenTemplateFoot: { def: true },
|
||||
matugenTemplateAlacritty: { def: true },
|
||||
matugenTemplateNeovim: { def: false },
|
||||
matugenTemplateNeovim: { def: true },
|
||||
matugenTemplateWezterm: { def: true },
|
||||
matugenTemplateDgop: { def: true },
|
||||
matugenTemplateKcolorscheme: { def: true },
|
||||
matugenTemplateVscode: { 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 },
|
||||
dockAutoHide: { def: false },
|
||||
dockSmartAutoHide: { def: false },
|
||||
dockGroupByApp: { def: false },
|
||||
dockRestoreSpecialWorkspaceOnClick: { def: false },
|
||||
dockOpenOnOverview: { def: false },
|
||||
dockPosition: { def: 1 },
|
||||
dockSpacing: { def: 4 },
|
||||
@@ -355,23 +330,9 @@ var SPEC = {
|
||||
enableFprint: { def: false },
|
||||
maxFprintTries: { def: 15 },
|
||||
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 },
|
||||
u2fMode: { def: "or" },
|
||||
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" },
|
||||
lockScreenInactiveColor: { def: "#000000" },
|
||||
lockScreenNotificationMode: { def: 0 },
|
||||
@@ -394,7 +355,6 @@ var SPEC = {
|
||||
notificationHistorySaveNormal: { def: true },
|
||||
notificationHistorySaveCritical: { def: true },
|
||||
notificationRules: { def: [] },
|
||||
notificationFocusedMonitor: { def: false },
|
||||
|
||||
osdAlwaysShowValue: { def: false },
|
||||
osdPosition: { def: 5 },
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Greetd
|
||||
import qs.Common
|
||||
import qs.Modules.Greetd
|
||||
|
||||
Scope {
|
||||
|
||||
@@ -313,7 +313,7 @@ Item {
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: SettingsData.notificationFocusedMonitor ? Quickshell.screens : SettingsData.getFilteredScreens("notifications")
|
||||
model: SettingsData.getFilteredScreens("notifications")
|
||||
|
||||
delegate: NotificationPopupManager {
|
||||
modelData: item
|
||||
@@ -619,10 +619,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
MuxModal {
|
||||
id: muxModal
|
||||
}
|
||||
|
||||
ClipboardHistoryModal {
|
||||
id: clipboardHistoryModalPopup
|
||||
|
||||
@@ -819,8 +815,9 @@ Item {
|
||||
|
||||
content: Component {
|
||||
Notepad {
|
||||
slideout: notepadSlideout
|
||||
onHideRequested: notepadSlideout.hide()
|
||||
onHideRequested: {
|
||||
notepadSlideout.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,9 +26,7 @@ Rectangle {
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: keyboardHints.enterToPaste
|
||||
? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled")
|
||||
: I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help")
|
||||
text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled") : "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
@@ -26,10 +26,10 @@ Item {
|
||||
property bool closeOnBackgroundClick: true
|
||||
property string animationType: "scale"
|
||||
property int animationDuration: Theme.modalAnimationDuration
|
||||
property real animationScaleCollapsed: 0.96
|
||||
property real animationOffset: Theme.spacingL
|
||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||
property real animationScaleCollapsed: Theme.effectScaleCollapsed
|
||||
property real animationOffset: Theme.effectAnimOffset
|
||||
property list<real> animationEnterCurve: Theme.variantModalEnterCurve
|
||||
property list<real> animationExitCurve: Theme.variantModalExitCurve
|
||||
property color backgroundColor: Theme.surfaceContainer
|
||||
property color borderColor: Theme.outlineMedium
|
||||
property real borderWidth: 0
|
||||
@@ -44,11 +44,13 @@ Item {
|
||||
property bool keepPopoutsOpen: false
|
||||
property var customKeyboardFocus: null
|
||||
property bool useOverlayLayer: false
|
||||
property real frozenMotionOffsetX: 0
|
||||
property real frozenMotionOffsetY: 0
|
||||
readonly property alias contentWindow: contentWindow
|
||||
readonly property alias clickCatcher: clickCatcher
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
|
||||
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground
|
||||
readonly property bool useSingleWindow: CompositorService.isHyprland
|
||||
|
||||
signal opened
|
||||
signal dialogClosed
|
||||
@@ -58,19 +60,34 @@ Item {
|
||||
|
||||
function open() {
|
||||
closeTimer.stop();
|
||||
animationsEnabled = false;
|
||||
frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0;
|
||||
frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset;
|
||||
|
||||
const focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen) {
|
||||
contentWindow.screen = focusedScreen;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.screen = focusedScreen;
|
||||
}
|
||||
|
||||
if (Theme.isDirectionalEffect || root.useBackground) {
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = true;
|
||||
contentWindow.visible = true;
|
||||
}
|
||||
ModalManager.openModal(root);
|
||||
shouldBeVisible = true;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = true;
|
||||
contentWindow.visible = true;
|
||||
shouldHaveFocus = false;
|
||||
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
|
||||
|
||||
Qt.callLater(() => {
|
||||
animationsEnabled = true;
|
||||
shouldBeVisible = true;
|
||||
if (!useSingleWindow && !clickCatcher.visible)
|
||||
clickCatcher.visible = true;
|
||||
if (!contentWindow.visible)
|
||||
contentWindow.visible = true;
|
||||
shouldHaveFocus = false;
|
||||
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
|
||||
});
|
||||
}
|
||||
|
||||
function close() {
|
||||
@@ -131,7 +148,7 @@ Item {
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: animationDuration + 50
|
||||
interval: Theme.variantCloseInterval(animationDuration)
|
||||
onTriggered: {
|
||||
if (shouldBeVisible)
|
||||
return;
|
||||
@@ -145,7 +162,17 @@ Item {
|
||||
readonly property var shadowLevel: Theme.elevationLevel3
|
||||
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 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 alignedWidth: Theme.px(modalWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
||||
@@ -205,9 +232,26 @@ Item {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
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 {
|
||||
@@ -250,9 +294,12 @@ Item {
|
||||
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 {
|
||||
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
|
||||
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
|
||||
left: actualMarginLeft
|
||||
top: actualMarginTop
|
||||
right: 0
|
||||
bottom: 0
|
||||
}
|
||||
@@ -282,13 +329,14 @@ Item {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
color: "black"
|
||||
opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: root.useBackground
|
||||
opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -296,8 +344,8 @@ Item {
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: root.useSingleWindow ? root.alignedX : shadowBuffer
|
||||
y: root.useSingleWindow ? root.alignedY : shadowBuffer
|
||||
x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr)
|
||||
y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr)
|
||||
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
@@ -313,45 +361,117 @@ Item {
|
||||
}
|
||||
|
||||
readonly property bool slide: root.animationType === "slide"
|
||||
readonly property real offsetX: slide ? 15 : 0
|
||||
readonly property real offsetY: slide ? -30 : root.animationOffset
|
||||
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: root.animationScaleCollapsed
|
||||
|
||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
|
||||
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
|
||||
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
|
||||
readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
|
||||
readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
|
||||
readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
|
||||
readonly property real customDistLeft: customAnchorX
|
||||
readonly property real customDistRight: root.screenWidth - customAnchorX
|
||||
readonly property real customDistTop: customAnchorY
|
||||
readonly property real customDistBottom: root.screenHeight - customAnchorY
|
||||
readonly property real offsetX: {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
|
||||
return 0;
|
||||
if (slide && !directionalEffect && !depthEffect)
|
||||
return 15;
|
||||
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 {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animY {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
@@ -367,15 +487,14 @@ Item {
|
||||
id: animatedContent
|
||||
anchors.fill: parent
|
||||
clip: false
|
||||
opacity: root.shouldBeVisible ? 1 : 0
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0)
|
||||
scale: modalContainer.scaleValue
|
||||
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
|
||||
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled
|
||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:input-modal"
|
||||
keepPopoutsOpen: true
|
||||
|
||||
property string inputTitle: ""
|
||||
property string inputMessage: ""
|
||||
property string inputPlaceholder: ""
|
||||
property string inputText: ""
|
||||
property string confirmButtonText: "Confirm"
|
||||
property string cancelButtonText: "Cancel"
|
||||
property color confirmButtonColor: Theme.primary
|
||||
property var onConfirm: function (text) {}
|
||||
property var onCancel: function () {}
|
||||
property int selectedButton: -1
|
||||
property bool keyboardNavigation: false
|
||||
|
||||
function show(title, message, onConfirmCallback, onCancelCallback) {
|
||||
inputTitle = title || "";
|
||||
inputMessage = message || "";
|
||||
inputPlaceholder = "";
|
||||
inputText = "";
|
||||
confirmButtonText = "Confirm";
|
||||
cancelButtonText = "Cancel";
|
||||
confirmButtonColor = Theme.primary;
|
||||
onConfirm = onConfirmCallback || ((text) => {});
|
||||
onCancel = onCancelCallback || (() => {});
|
||||
selectedButton = -1;
|
||||
keyboardNavigation = false;
|
||||
open();
|
||||
}
|
||||
|
||||
function showWithOptions(options) {
|
||||
inputTitle = options.title || "";
|
||||
inputMessage = options.message || "";
|
||||
inputPlaceholder = options.placeholder || "";
|
||||
inputText = options.initialText || "";
|
||||
confirmButtonText = options.confirmText || "Confirm";
|
||||
cancelButtonText = options.cancelText || "Cancel";
|
||||
confirmButtonColor = options.confirmColor || Theme.primary;
|
||||
onConfirm = options.onConfirm || ((text) => {});
|
||||
onCancel = options.onCancel || (() => {});
|
||||
selectedButton = -1;
|
||||
keyboardNavigation = false;
|
||||
open();
|
||||
}
|
||||
|
||||
function confirmAndClose() {
|
||||
const text = inputText;
|
||||
close();
|
||||
if (onConfirm) {
|
||||
onConfirm(text);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelAndClose() {
|
||||
close();
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
function selectButton() {
|
||||
if (selectedButton === 0) {
|
||||
cancelAndClose();
|
||||
} else {
|
||||
confirmAndClose();
|
||||
}
|
||||
}
|
||||
|
||||
shouldBeVisible: false
|
||||
allowStacking: true
|
||||
modalWidth: 350
|
||||
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200
|
||||
enableShadow: true
|
||||
shouldHaveFocus: true
|
||||
onBackgroundClicked: cancelAndClose()
|
||||
onOpened: {
|
||||
Qt.callLater(function () {
|
||||
if (contentLoader.item && contentLoader.item.textInputRef) {
|
||||
contentLoader.item.textInputRef.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
implicitHeight: mainColumn.implicitHeight
|
||||
focus: true
|
||||
|
||||
property alias textInputRef: textInput
|
||||
|
||||
Keys.onPressed: function (event) {
|
||||
const textFieldFocused = textInput.activeFocus;
|
||||
|
||||
switch (event.key) {
|
||||
case Qt.Key_Escape:
|
||||
root.cancelAndClose();
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Tab:
|
||||
if (textFieldFocused) {
|
||||
root.keyboardNavigation = true;
|
||||
root.selectedButton = 0;
|
||||
textInput.focus = false;
|
||||
} else {
|
||||
root.keyboardNavigation = true;
|
||||
if (root.selectedButton === -1) {
|
||||
root.selectedButton = 0;
|
||||
} else if (root.selectedButton === 0) {
|
||||
root.selectedButton = 1;
|
||||
} else {
|
||||
root.selectedButton = -1;
|
||||
textInput.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Left:
|
||||
if (!textFieldFocused) {
|
||||
root.keyboardNavigation = true;
|
||||
root.selectedButton = 0;
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_Right:
|
||||
if (!textFieldFocused) {
|
||||
root.keyboardNavigation = true;
|
||||
root.selectedButton = 1;
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
if (root.selectedButton !== -1) {
|
||||
root.selectButton();
|
||||
} else {
|
||||
root.confirmAndClose();
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.topMargin: Theme.spacingL
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
text: root.inputTitle
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.spacingL
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.inputMessage
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.inputMessage !== ""
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: root.inputMessage !== "" ? Theme.spacingL : 0
|
||||
visible: root.inputMessage !== ""
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceVariantAlpha
|
||||
border.color: textInput.activeFocus ? Theme.primary : "transparent"
|
||||
border.width: textInput.activeFocus ? 1 : 0
|
||||
|
||||
TextInput {
|
||||
id: textInput
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
selectionColor: Theme.primary
|
||||
selectedTextColor: Theme.primaryText
|
||||
clip: true
|
||||
text: root.inputText
|
||||
onTextChanged: root.inputText = text
|
||||
|
||||
StyledText {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
||||
text: root.inputPlaceholder
|
||||
visible: textInput.text === "" && !textInput.activeFocus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.spacingL * 1.5
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (root.keyboardNavigation && root.selectedButton === 0) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
} else if (cancelButton.containsMouse) {
|
||||
return Theme.surfacePressed;
|
||||
} else {
|
||||
return Theme.surfaceVariantAlpha;
|
||||
}
|
||||
}
|
||||
border.color: (root.keyboardNavigation && root.selectedButton === 0) ? Theme.primary : "transparent"
|
||||
border.width: (root.keyboardNavigation && root.selectedButton === 0) ? 1 : 0
|
||||
|
||||
StyledText {
|
||||
text: root.cancelButtonText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.cancelAndClose()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
const baseColor = root.confirmButtonColor;
|
||||
if (root.keyboardNavigation && root.selectedButton === 1) {
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1);
|
||||
} else if (confirmButton.containsMouse) {
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9);
|
||||
} else {
|
||||
return baseColor;
|
||||
}
|
||||
}
|
||||
border.color: (root.keyboardNavigation && root.selectedButton === 1) ? "white" : "transparent"
|
||||
border.width: (root.keyboardNavigation && root.selectedButton === 1) ? 1 : 0
|
||||
|
||||
StyledText {
|
||||
text: root.confirmButtonText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primaryText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: confirmButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.confirmAndClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.spacingL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,13 +147,6 @@ DankModal {
|
||||
return "COLOR_PICKER_MODAL_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function openColor(color: string): string {
|
||||
root.selectedColor = Qt.color(color);
|
||||
root.currentColor = Qt.color(color);
|
||||
root.updateFromColor(Qt.color(color));
|
||||
return open();
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
root.hide();
|
||||
return "COLOR_PICKER_MODAL_CLOSE_SUCCESS";
|
||||
|
||||
@@ -207,12 +207,9 @@ Rectangle {
|
||||
selectedActionIndex = 0;
|
||||
}
|
||||
|
||||
function cycleAction(reverse = false) {
|
||||
function cycleAction() {
|
||||
if (actions.length > 0) {
|
||||
if (! reverse)
|
||||
selectedActionIndex = (selectedActionIndex + 1) % actions.length;
|
||||
else
|
||||
selectedActionIndex = (selectedActionIndex - 1) % actions.length;
|
||||
selectedActionIndex = (selectedActionIndex + 1) % actions.length;
|
||||
ensureSelectedVisible();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,13 +353,10 @@ Item {
|
||||
performSearch();
|
||||
}
|
||||
|
||||
function cycleMode(reverse = false) {
|
||||
function cycleMode() {
|
||||
var modes = ["all", "apps", "files", "plugins"];
|
||||
var currentIndex = modes.indexOf(searchMode);
|
||||
if (!reverse)
|
||||
var nextIndex = (currentIndex + 1) % modes.length;
|
||||
else
|
||||
var nextIndex = (currentIndex - 1 + modes.length) % modes.length;
|
||||
var nextIndex = (currentIndex + 1) % modes.length;
|
||||
setMode(modes[nextIndex]);
|
||||
}
|
||||
|
||||
@@ -1009,7 +1006,9 @@ Item {
|
||||
_applyHighlights(newSections, searchQuery);
|
||||
flatModel = Scorer.flattenSections(newSections);
|
||||
sections = newSections;
|
||||
selectedFlatIndex = getFirstItemIndex();
|
||||
if (selectedFlatIndex >= flatModel.length) {
|
||||
selectedFlatIndex = getFirstItemIndex();
|
||||
}
|
||||
updateSelectedItem();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
@@ -13,16 +14,24 @@ Item {
|
||||
property bool spotlightOpen: false
|
||||
property bool keyboardActive: false
|
||||
property bool contentVisible: false
|
||||
readonly property bool launcherMotionVisible: Theme.isDirectionalEffect ? spotlightOpen : _motionActive
|
||||
property var spotlightContent: launcherContentLoader.item
|
||||
property bool openedFromOverview: false
|
||||
property bool isClosing: false
|
||||
property bool _windowEnabled: true
|
||||
property bool _pendingInitialize: false
|
||||
property string _pendingQuery: ""
|
||||
property string _pendingMode: ""
|
||||
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 var effectiveScreen: launcherWindow.screen
|
||||
readonly property var effectiveScreen: contentWindow.screen
|
||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||
@@ -76,6 +85,34 @@ Item {
|
||||
}
|
||||
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
|
||||
|
||||
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||
@@ -95,7 +132,8 @@ Item {
|
||||
if (!spotlightContent)
|
||||
return;
|
||||
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) {
|
||||
spotlightContent.searchField.text = query;
|
||||
@@ -128,40 +166,59 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
function _openCommon(query, mode) {
|
||||
closeCleanupTimer.stop();
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen)
|
||||
launcherWindow.screen = focusedScreen;
|
||||
// Disable animations so the snap is instant
|
||||
animationsEnabled = false;
|
||||
|
||||
spotlightOpen = true;
|
||||
keyboardActive = true;
|
||||
// Freeze the collapsed offsets (they depend on height which could change)
|
||||
_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);
|
||||
spotlightOpen = true;
|
||||
backgroundWindow.visible = true;
|
||||
contentWindow.visible = true;
|
||||
if (useHyprlandFocusGrab)
|
||||
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) {
|
||||
closeCleanupTimer.stop();
|
||||
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, "");
|
||||
_openCommon(query, "");
|
||||
}
|
||||
|
||||
function hide() {
|
||||
@@ -169,13 +226,17 @@ Item {
|
||||
return;
|
||||
openedFromOverview = false;
|
||||
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;
|
||||
spotlightOpen = false;
|
||||
focusGrab.active = false;
|
||||
ModalManager.closeModal(root);
|
||||
|
||||
closeCleanupTimer.start();
|
||||
}
|
||||
|
||||
@@ -184,21 +245,7 @@ Item {
|
||||
}
|
||||
|
||||
function showWithMode(mode) {
|
||||
closeCleanupTimer.stop();
|
||||
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);
|
||||
_openCommon("", mode);
|
||||
}
|
||||
|
||||
function toggleWithMode(mode) {
|
||||
@@ -219,10 +266,13 @@ Item {
|
||||
|
||||
Timer {
|
||||
id: closeCleanupTimer
|
||||
interval: Theme.modalAnimationDuration + 50
|
||||
interval: Theme.variantCloseInterval(Theme.modalAnimationDuration)
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
isClosing = false;
|
||||
contentVisible = false;
|
||||
contentWindow.visible = false;
|
||||
backgroundWindow.visible = false;
|
||||
if (root.unloadContentOnClose)
|
||||
launcherContentLoader.active = false;
|
||||
dialogClosed();
|
||||
@@ -240,7 +290,7 @@ Item {
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
windows: [launcherWindow]
|
||||
windows: [contentWindow]
|
||||
active: false
|
||||
|
||||
onCleared: {
|
||||
@@ -265,45 +315,53 @@ Item {
|
||||
if (Quickshell.screens.length === 0)
|
||||
return;
|
||||
|
||||
const screenName = launcherWindow.screen?.name;
|
||||
if (screenName) {
|
||||
const screen = contentWindow.screen;
|
||||
const screenName = screen?.name;
|
||||
|
||||
let needsReset = !screen || !screenName;
|
||||
if (!needsReset) {
|
||||
needsReset = true;
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
if (Quickshell.screens[i].name === screenName)
|
||||
return;
|
||||
if (Quickshell.screens[i].name === screenName) {
|
||||
needsReset = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (spotlightOpen)
|
||||
hide();
|
||||
if (!needsReset)
|
||||
return;
|
||||
|
||||
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
|
||||
if (newScreen)
|
||||
launcherWindow.screen = newScreen;
|
||||
if (!newScreen)
|
||||
return;
|
||||
|
||||
root._windowEnabled = false;
|
||||
backgroundWindow.screen = newScreen;
|
||||
contentWindow.screen = newScreen;
|
||||
Qt.callLater(() => {
|
||||
root._windowEnabled = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Background window: fullscreen, handles darkening + click-to-dismiss ──
|
||||
PanelWindow {
|
||||
id: launcherWindow
|
||||
visible: spotlightOpen || isClosing
|
||||
id: backgroundWindow
|
||||
visible: false
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight"
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
case "bottom":
|
||||
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "background":
|
||||
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay;
|
||||
default:
|
||||
return WlrLayershell.Top;
|
||||
}
|
||||
WlrLayershell.namespace: "dms:spotlight:bg"
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
WlrLayershell.margins {
|
||||
top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
|
||||
bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
|
||||
left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
|
||||
right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
|
||||
}
|
||||
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
@@ -313,11 +371,11 @@ Item {
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: spotlightOpen ? fullScreenMask : null
|
||||
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
|
||||
}
|
||||
|
||||
Item {
|
||||
id: fullScreenMask
|
||||
id: bgFullScreenMask
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
@@ -325,13 +383,14 @@ Item {
|
||||
id: backgroundDarken
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
||||
visible: contentVisible || opacity > 0
|
||||
opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
||||
visible: launcherMotionVisible || opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||
DankAnim {
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,88 +398,240 @@ Item {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: spotlightOpen
|
||||
onClicked: mouse => {
|
||||
var contentX = modalContainer.x;
|
||||
var contentY = modalContainer.y;
|
||||
var contentW = modalContainer.width;
|
||||
var contentH = modalContainer.height;
|
||||
onClicked: root.hide()
|
||||
}
|
||||
}
|
||||
|
||||
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
|
||||
root.hide();
|
||||
}
|
||||
// ── Content window: SMALL, positioned with margins — only renders the modal area ──
|
||||
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 {
|
||||
id: modalContainer
|
||||
x: root.modalX
|
||||
y: root.modalY
|
||||
width: root.modalWidth
|
||||
height: root.modalHeight
|
||||
visible: contentVisible || opacity > 0
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
id: contentInputMask
|
||||
visible: false
|
||||
x: contentContainer.x + contentWrapper.x
|
||||
y: contentContainer.y + contentWrapper.y
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
|
||||
// For directional/depth: contentContainer is at alignedY from window top (window starts at screen top)
|
||||
// For standard: contentContainer is at shadowPad from window top (window starts near modal)
|
||||
x: root._ccX
|
||||
y: root._ccY
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
|
||||
readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1
|
||||
readonly property bool dockTop: dockEdge === 0
|
||||
readonly property bool dockBottom: dockEdge === 1
|
||||
readonly property bool dockLeft: dockEdge === 2
|
||||
readonly property bool dockRight: dockEdge === 3
|
||||
|
||||
readonly property real dockThickness: typeof SettingsData !== "undefined" && SettingsData.showDock ? Theme.px(SettingsData.dockIconSize + (SettingsData.dockMargin * 2) + SettingsData.dockSpacing + 8, root.dpr) : Theme.px(60, root.dpr)
|
||||
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
readonly property real collapsedMotionX: {
|
||||
if (directionalEffect) {
|
||||
if (dockLeft)
|
||||
return -(root._ccX + root.alignedWidth + Theme.effectAnimOffset);
|
||||
if (dockRight)
|
||||
return root.screenWidth - root._ccX + Theme.effectAnimOffset;
|
||||
}
|
||||
if (depthEffect)
|
||||
return Theme.effectAnimOffset * 0.25;
|
||||
return 0;
|
||||
}
|
||||
readonly property real collapsedMotionY: {
|
||||
if (directionalEffect) {
|
||||
if (dockTop)
|
||||
return -(root._ccY + root.alignedHeight + Theme.effectAnimOffset);
|
||||
if (dockBottom)
|
||||
return root.screenHeight - root._ccY + root.shadowPad + Theme.effectAnimOffset;
|
||||
return 0;
|
||||
}
|
||||
if (depthEffect)
|
||||
return -Math.max(Theme.effectAnimOffset * 0.85, 34);
|
||||
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
|
||||
}
|
||||
|
||||
// animX/animY are Behavior-animated — DankPopout pattern
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed)
|
||||
|
||||
Component.onCompleted: {
|
||||
animX = Theme.snap(root._motionActive ? 0 : collapsedMotionX, root.dpr);
|
||||
animY = Theme.snap(root._motionActive ? 0 : collapsedMotionY, root.dpr);
|
||||
scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function on_MotionActiveChanged() {
|
||||
contentContainer.animX = Theme.snap(root._motionActive ? 0 : root._frozenMotionX, root.dpr);
|
||||
contentContainer.animY = Theme.snap(root._motionActive ? 0 : root._frozenMotionY, root.dpr);
|
||||
contentContainer.scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animX {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
|
||||
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animY {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
|
||||
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2))
|
||||
DankAnim {
|
||||
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
|
||||
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: directionalClipMask
|
||||
readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0
|
||||
readonly property real clipOversize: 2000
|
||||
|
||||
clip: shouldClip
|
||||
|
||||
x: shouldClip ? (contentContainer.dockRight ? -clipOversize : (contentContainer.dockLeft ? contentContainer.dockThickness - root._ccX : -clipOversize)) : 0
|
||||
y: shouldClip ? (contentContainer.dockBottom ? -clipOversize : (contentContainer.dockTop ? contentContainer.dockThickness - root._ccY : -clipOversize)) : 0
|
||||
|
||||
width: shouldClip ? parent.width + clipOversize + (contentContainer.dockRight ? (root.screenWidth - contentContainer.dockThickness - root._ccX - parent.width) : (contentContainer.dockLeft ? clipOversize : clipOversize)) : parent.width
|
||||
height: shouldClip ? parent.height + clipOversize + (contentContainer.dockBottom ? (root.screenHeight - contentContainer.dockThickness - root._ccY - parent.height) : (contentContainer.dockTop ? clipOversize : clipOversize)) : parent.height
|
||||
|
||||
Item {
|
||||
id: aligner
|
||||
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
|
||||
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
|
||||
width: contentContainer.width
|
||||
height: contentContainer.height
|
||||
|
||||
// Shadow mirrors contentWrapper position/scale/opacity
|
||||
ElevationShadow {
|
||||
id: launcherShadowLayer
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: contentWrapper.opacity
|
||||
scale: contentWrapper.scale
|
||||
x: contentWrapper.x
|
||||
y: contentWrapper.y
|
||||
level: root.shadowLevel
|
||||
fallbackOffset: root.shadowFallbackOffset
|
||||
targetColor: root.backgroundColor
|
||||
borderColor: root.borderColor
|
||||
borderWidth: root.borderWidth
|
||||
targetRadius: root.cornerRadius
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
}
|
||||
|
||||
// contentWrapper moves inside static contentContainer — DankPopout pattern
|
||||
Item {
|
||||
id: contentWrapper
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (launcherMotionVisible ? 1 : 0)
|
||||
visible: opacity > 0
|
||||
scale: contentContainer.scaleValue
|
||||
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||
DankAnim {
|
||||
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => mouse.accepted = true
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: keyboardActive
|
||||
|
||||
Loader {
|
||||
id: launcherContentLoader
|
||||
anchors.fill: parent
|
||||
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
|
||||
asynchronous: false
|
||||
sourceComponent: LauncherContent {
|
||||
focus: true
|
||||
parentModal: root
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (root._pendingInitialize) {
|
||||
root._initializeAndShow(root._pendingQuery, root._pendingMode);
|
||||
root._pendingInitialize = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
root.hide();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
} // contentWrapper
|
||||
} // aligner
|
||||
} // directionalClipMask
|
||||
} // contentContainer
|
||||
} // PanelWindow
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ FocusScope {
|
||||
editCommentField.text = existing?.comment || "";
|
||||
editEnvVarsField.text = existing?.envVars || "";
|
||||
editExtraFlagsField.text = existing?.extraFlags || "";
|
||||
editDgpuToggle.checked = existing?.launchOnDgpu || false;
|
||||
editMode = true;
|
||||
Qt.callLater(() => editNameField.forceActiveFocus());
|
||||
}
|
||||
@@ -65,8 +64,6 @@ FocusScope {
|
||||
override.envVars = editEnvVarsField.text.trim();
|
||||
if (editExtraFlagsField.text.trim())
|
||||
override.extraFlags = editExtraFlagsField.text.trim();
|
||||
if (editDgpuToggle.checked)
|
||||
override.launchOnDgpu = true;
|
||||
SessionData.setAppOverride(editAppId, override);
|
||||
closeEditMode();
|
||||
}
|
||||
@@ -89,7 +86,7 @@ FocusScope {
|
||||
|
||||
Controller {
|
||||
id: controller
|
||||
active: root.parentModal?.spotlightOpen ?? true
|
||||
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
||||
viewModeContext: root.viewModeContext
|
||||
|
||||
onItemExecuted: {
|
||||
@@ -161,10 +158,6 @@ FocusScope {
|
||||
controller.selectPageUp(8);
|
||||
return;
|
||||
case Qt.Key_Right:
|
||||
if (hasCtrl) {
|
||||
controller.cycleMode();
|
||||
return;
|
||||
}
|
||||
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||
controller.selectRight();
|
||||
return;
|
||||
@@ -172,25 +165,12 @@ FocusScope {
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_Left:
|
||||
if (hasCtrl) {
|
||||
const reverse = true;
|
||||
controller.cycleMode(reverse);
|
||||
return;
|
||||
}
|
||||
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||
controller.selectLeft();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_H:
|
||||
if (hasCtrl) {
|
||||
const reverse = true;
|
||||
controller.cycleMode(reverse);
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_J:
|
||||
if (hasCtrl) {
|
||||
controller.selectNext();
|
||||
@@ -205,13 +185,6 @@ FocusScope {
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_L:
|
||||
if (hasCtrl) {
|
||||
controller.cycleMode();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_N:
|
||||
if (hasCtrl) {
|
||||
controller.selectNextSection();
|
||||
@@ -227,19 +200,13 @@ FocusScope {
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_Tab:
|
||||
if (hasCtrl && actionPanel.hasActions) {
|
||||
if (actionPanel.hasActions) {
|
||||
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
|
||||
return;
|
||||
}
|
||||
controller.selectNext();
|
||||
return;
|
||||
case Qt.Key_Backtab:
|
||||
if (hasCtrl && actionPanel.expanded) {
|
||||
const reverse = true;
|
||||
actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show();
|
||||
return;
|
||||
}
|
||||
controller.selectPrevious();
|
||||
if (actionPanel.expanded)
|
||||
actionPanel.hide();
|
||||
return;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
@@ -421,7 +388,7 @@ FocusScope {
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Ctrl-Tab " + I18n.tr("actions")
|
||||
text: "Tab " + I18n.tr("actions")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
visible: actionPanel.hasActions
|
||||
@@ -495,7 +462,7 @@ FocusScope {
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
enabled: root.parentModal ? root.parentModal.spotlightOpen : true
|
||||
enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
||||
placeholderText: ""
|
||||
ignoreUpDownKeys: true
|
||||
ignoreTabKeys: true
|
||||
@@ -729,7 +696,13 @@ FocusScope {
|
||||
Item {
|
||||
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)
|
||||
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 {
|
||||
id: resultsList
|
||||
@@ -973,15 +946,6 @@ FocusScope {
|
||||
keyNavigationBacktab: editEnvVarsField
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: editDgpuToggle
|
||||
width: parent.width
|
||||
text: I18n.tr("Launch on dGPU by default")
|
||||
visible: SessionService.nvidiaCommand.length > 0
|
||||
checked: false
|
||||
onToggled: checked => editDgpuToggle.checked = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -468,7 +468,7 @@ Item {
|
||||
switch (mode) {
|
||||
case "files":
|
||||
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)
|
||||
return I18n.tr("Type to search files");
|
||||
if (root.controller.searchQuery.length < 2)
|
||||
|
||||
@@ -81,7 +81,7 @@ DankModal {
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
text: KeybindsService.cheatsheet.title || i18n("Keybinds")
|
||||
text: KeybindsService.cheatsheet.title || "Keybinds"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
@@ -309,12 +309,10 @@ DankModal {
|
||||
id: keyText
|
||||
anchors.centerIn: parent
|
||||
color: Theme.secondary
|
||||
text: (modelData.key || "").replace(/\+/g, " + ")
|
||||
text: modelData.key || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
isMonospace: true
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, 148)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +325,6 @@ DankModal {
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
opacity: 0.9
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,633 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: muxModal
|
||||
|
||||
layerNamespace: "dms:mux"
|
||||
|
||||
property int selectedIndex: -1
|
||||
property string searchText: ""
|
||||
property var filteredSessions: []
|
||||
|
||||
function updateFilteredSessions() {
|
||||
var filtered = []
|
||||
var lowerSearch = searchText.trim().toLowerCase()
|
||||
for (var i = 0; i < MuxService.sessions.length; i++) {
|
||||
var session = MuxService.sessions[i]
|
||||
if (lowerSearch.length > 0 && !session.name.toLowerCase().includes(lowerSearch))
|
||||
continue
|
||||
filtered.push(session)
|
||||
}
|
||||
filteredSessions = filtered
|
||||
|
||||
if (selectedIndex >= filteredSessions.length) {
|
||||
selectedIndex = Math.max(0, filteredSessions.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
onSearchTextChanged: updateFilteredSessions()
|
||||
|
||||
Connections {
|
||||
target: MuxService
|
||||
function onSessionsChanged() {
|
||||
updateFilteredSessions()
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
windows: [muxModal.contentWindow]
|
||||
active: CompositorService.isHyprland && muxModal.shouldHaveFocus
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
hide()
|
||||
} else {
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
open()
|
||||
selectedIndex = -1
|
||||
searchText = ""
|
||||
MuxService.refreshSessions()
|
||||
shouldHaveFocus = true
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (muxPanel && muxPanel.searchField) {
|
||||
muxPanel.searchField.forceActiveFocus();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function hide() {
|
||||
close()
|
||||
selectedIndex = -1
|
||||
searchText = ""
|
||||
}
|
||||
|
||||
function attachToSession(name) {
|
||||
MuxService.attachToSession(name)
|
||||
hide()
|
||||
}
|
||||
|
||||
function renameSession(name) {
|
||||
inputModal.showWithOptions({
|
||||
title: I18n.tr("Rename Session"),
|
||||
message: I18n.tr("Enter a new name for session \"%1\"").arg(name),
|
||||
initialText: name,
|
||||
onConfirm: function (newName) {
|
||||
MuxService.renameSession(name, newName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function killSession(name) {
|
||||
confirmModal.showWithOptions({
|
||||
title: I18n.tr("Kill Session"),
|
||||
message: I18n.tr("Are you sure you want to kill session \"%1\"?").arg(name),
|
||||
confirmText: I18n.tr("Kill"),
|
||||
confirmColor: Theme.primary,
|
||||
onConfirm: function () {
|
||||
MuxService.killSession(name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function createNewSession() {
|
||||
inputModal.showWithOptions({
|
||||
title: I18n.tr("New Session"),
|
||||
message: I18n.tr("Please write a name for your new %1 session").arg(MuxService.displayName),
|
||||
onConfirm: function (name) {
|
||||
MuxService.createSession(name)
|
||||
hide()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
selectedIndex = Math.min(selectedIndex + 1, filteredSessions.length - 1)
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
selectedIndex = Math.max(selectedIndex - 1, -1)
|
||||
}
|
||||
|
||||
function activateSelected() {
|
||||
if (selectedIndex === -1) {
|
||||
createNewSession()
|
||||
} else if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
|
||||
attachToSession(filteredSessions[selectedIndex].name)
|
||||
}
|
||||
}
|
||||
|
||||
visible: false
|
||||
modalWidth: 600
|
||||
modalHeight: 600
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
cornerRadius: Theme.cornerRadius
|
||||
borderColor: Theme.outlineMedium
|
||||
borderWidth: 1
|
||||
enableShadow: true
|
||||
keepContentLoaded: true
|
||||
|
||||
onBackgroundClicked: hide()
|
||||
|
||||
Timer {
|
||||
interval: 3000
|
||||
running: muxModal.shouldBeVisible
|
||||
repeat: true
|
||||
onTriggered: MuxService.refreshSessions()
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
muxModal.show()
|
||||
return "MUX_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
muxModal.hide()
|
||||
return "MUX_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
muxModal.toggle()
|
||||
return "MUX_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "mux"
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
muxModal.show()
|
||||
return "TMUX_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
muxModal.hide()
|
||||
return "TMUX_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
muxModal.toggle()
|
||||
return "TMUX_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "tmux"
|
||||
}
|
||||
|
||||
InputModal {
|
||||
id: inputModal
|
||||
onShouldBeVisibleChanged: {
|
||||
if (shouldBeVisible) {
|
||||
muxModal.shouldHaveFocus = false;
|
||||
muxModal.contentWindow.visible = false;
|
||||
return;
|
||||
}
|
||||
if (muxModal.shouldBeVisible) {
|
||||
muxModal.contentWindow.visible = true;
|
||||
}
|
||||
Qt.callLater(function () {
|
||||
if (!muxModal.shouldBeVisible) {
|
||||
return;
|
||||
}
|
||||
muxModal.shouldHaveFocus = true;
|
||||
muxModal.modalFocusScope.forceActiveFocus();
|
||||
if (muxPanel.searchField) {
|
||||
muxPanel.searchField.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmModal {
|
||||
id: confirmModal
|
||||
onShouldBeVisibleChanged: {
|
||||
if (shouldBeVisible) {
|
||||
muxModal.shouldHaveFocus = false;
|
||||
muxModal.contentWindow.visible = false;
|
||||
return;
|
||||
}
|
||||
if (muxModal.shouldBeVisible) {
|
||||
muxModal.contentWindow.visible = true;
|
||||
}
|
||||
Qt.callLater(function () {
|
||||
if (!muxModal.shouldBeVisible) {
|
||||
return;
|
||||
}
|
||||
muxModal.shouldHaveFocus = true;
|
||||
muxModal.modalFocusScope.forceActiveFocus();
|
||||
if (muxPanel.searchField) {
|
||||
muxPanel.searchField.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
directContent: Item {
|
||||
id: muxPanel
|
||||
|
||||
clip: false
|
||||
|
||||
property alias searchField: searchField
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if ((event.key === Qt.Key_J && (event.modifiers & Qt.ControlModifier)) ||
|
||||
(event.key === Qt.Key_Down)) {
|
||||
selectNext()
|
||||
event.accepted = true
|
||||
} else if ((event.key === Qt.Key_K && (event.modifiers & Qt.ControlModifier)) ||
|
||||
(event.key === Qt.Key_Up)) {
|
||||
selectPrevious()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_N && (event.modifiers & Qt.ControlModifier)) {
|
||||
createNewSession()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_R && (event.modifiers & Qt.ControlModifier)) {
|
||||
if (MuxService.supportsRename && selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
|
||||
renameSession(filteredSessions[selectedIndex].name)
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_D && (event.modifiers & Qt.ControlModifier)) {
|
||||
if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
|
||||
killSession(filteredSessions[selectedIndex].name)
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Escape) {
|
||||
hide()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
activateSelected()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
height: parent.height - Theme.spacingM * 2
|
||||
x: Theme.spacingM
|
||||
y: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Header
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("%1 Sessions").arg(MuxService.displayName)
|
||||
font.pixelSize: Theme.fontSizeLarge + 4
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: {
|
||||
const total = MuxService.sessions.length;
|
||||
const filtered = muxModal.filteredSessions.length;
|
||||
const activePart = total === 1
|
||||
? I18n.tr("%1 active session").arg(total)
|
||||
: I18n.tr("%1 active sessions").arg(total);
|
||||
const filteredPart = filtered === 1
|
||||
? I18n.tr("%1 filtered").arg(filtered)
|
||||
: I18n.tr("%1 filtered").arg(filtered);
|
||||
return activePart + ", " + filteredPart;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
// Search field
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
width: parent.width
|
||||
height: 48
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Theme.surfaceContainerHigh
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
leftIconName: "search"
|
||||
leftIconSize: Theme.iconSize
|
||||
leftIconColor: Theme.surfaceVariantText
|
||||
leftIconFocusedColor: Theme.primary
|
||||
showClearButton: true
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
placeholderText: I18n.tr("Search sessions...")
|
||||
keyForwardTargets: [muxPanel]
|
||||
|
||||
onTextEdited: {
|
||||
muxModal.searchText = text
|
||||
muxModal.selectedIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
// New Session Button
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 56
|
||||
radius: Theme.cornerRadius
|
||||
color: muxModal.selectedIndex === -1 ? Theme.primaryContainer :
|
||||
(newMouse.containsMouse ? Theme.surfaceContainerHigh : Theme.surfaceContainer)
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 40
|
||||
Layout.preferredHeight: 40
|
||||
radius: 20
|
||||
color: Theme.primaryContainer
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "add"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("New Session")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Create a new %1 session (n)").arg(MuxService.displayName)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: newMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: muxModal.createNewSession()
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions List
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - 88 - 48 - shortcutsBar.height - Theme.spacingS * 3
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: muxModal.filteredSessions
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: parent.width
|
||||
height: 64
|
||||
radius: Theme.cornerRadius
|
||||
color: muxModal.selectedIndex === index ? Theme.primaryContainer :
|
||||
(sessionMouse.containsMouse ? Theme.surfaceContainerHigh : "transparent")
|
||||
|
||||
MouseArea {
|
||||
id: sessionMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: muxModal.attachToSession(modelData.name)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Avatar
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 40
|
||||
Layout.preferredHeight: 40
|
||||
radius: 20
|
||||
color: modelData.attached ? Theme.primaryContainer : Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.name.charAt(0).toUpperCase()
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: modelData.attached ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
// Info
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
var parts = []
|
||||
if (modelData.windows !== "N/A")
|
||||
parts.push(modelData.windows === 1
|
||||
? I18n.tr("%1 window").arg(modelData.windows)
|
||||
: I18n.tr("%1 windows").arg(modelData.windows))
|
||||
parts.push(modelData.attached ? I18n.tr("attached") : I18n.tr("detached"))
|
||||
return parts.join(" \u2022 ")
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
// Rename button (tmux only)
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
radius: 18
|
||||
visible: MuxService.supportsRename
|
||||
color: renameMouse.containsMouse ? Theme.surfaceContainerHighest : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "edit"
|
||||
size: Theme.iconSizeSmall
|
||||
color: renameMouse.containsMouse ? Theme.primary : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: renameMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: muxModal.renameSession(modelData.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete button
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
radius: 18
|
||||
color: deleteMouse.containsMouse ? Theme.errorContainer : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "delete"
|
||||
size: Theme.iconSizeSmall
|
||||
color: deleteMouse.containsMouse ? Theme.error : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deleteMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
muxModal.killSession(modelData.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state
|
||||
Item {
|
||||
width: parent.width
|
||||
height: muxModal.filteredSessions.length === 0 ? 200 : 0
|
||||
visible: muxModal.filteredSessions.length === 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: muxModal.searchText.length > 0 ? "search_off" : "terminal"
|
||||
size: 48
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: muxModal.searchText.length > 0 ? I18n.tr("No sessions found") : I18n.tr("No active %1 sessions").arg(MuxService.displayName)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: muxModal.searchText.length > 0 ? I18n.tr("Try a different search") : I18n.tr("Press 'n' or click 'New Session' to create one")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shortcuts bar
|
||||
Row {
|
||||
id: shortcutsBar
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
bottomPadding: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
var shortcuts = [
|
||||
{ key: "↑↓", label: I18n.tr("Navigate") },
|
||||
{ key: "↵", label: I18n.tr("Attach") },
|
||||
{ key: "^N", label: I18n.tr("New") },
|
||||
{ key: "^D", label: I18n.tr("Kill") },
|
||||
{ key: "Esc", label: I18n.tr("Close") }
|
||||
]
|
||||
if (MuxService.supportsRename)
|
||||
shortcuts.splice(3, 0, { key: "^R", label: I18n.tr("Rename") })
|
||||
return shortcuts
|
||||
}
|
||||
|
||||
delegate: Row {
|
||||
required property var modelData
|
||||
spacing: 4
|
||||
|
||||
Rectangle {
|
||||
width: keyText.width + Theme.spacingS
|
||||
height: keyText.height + 4
|
||||
radius: 4
|
||||
color: Theme.surfaceContainerHighest
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
id: keyText
|
||||
anchors.centerIn: parent
|
||||
text: modelData.key
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.label
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ DankModal {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Details for \"%1\"").arg(networkSSID)
|
||||
text: `Details for "${networkSSID}"`
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
@@ -102,7 +102,7 @@ DankModal {
|
||||
id: detailsText
|
||||
|
||||
width: parent.width
|
||||
text: NetworkService.networkInfoDetails && NetworkService.networkInfoDetails.replace(/\\n/g, '\n') || I18n.tr("No information available")
|
||||
text: NetworkService.networkInfoDetails && NetworkService.networkInfoDetails.replace(/\\n/g, '\n') || "No information available"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
@@ -66,7 +66,7 @@ DankModal {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Details for \"%1\"").arg(networkSSID)
|
||||
text: `Details for "${networkID}"`
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
@@ -102,7 +102,7 @@ DankModal {
|
||||
id: detailsText
|
||||
|
||||
width: parent.width
|
||||
text: NetworkService.networkWiredInfoDetails && NetworkService.networkWiredInfoDetails.replace(/\\n/g, '\n') || I18n.tr("No information available")
|
||||
text: NetworkService.networkWiredInfoDetails && NetworkService.networkWiredInfoDetails.replace(/\\n/g, '\n') || "No information available"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -12,45 +11,8 @@ FloatingWindow {
|
||||
property string passwordInput: ""
|
||||
property var currentFlow: PolkitService.agent?.flow
|
||||
property bool isLoading: false
|
||||
property bool awaitingFprintForPassword: false
|
||||
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() {
|
||||
passwordField.forceActiveFocus();
|
||||
}
|
||||
@@ -58,7 +20,6 @@ FloatingWindow {
|
||||
function show() {
|
||||
passwordInput = "";
|
||||
isLoading = false;
|
||||
awaitingFprintForPassword = false;
|
||||
visible = true;
|
||||
Qt.callLater(focusPasswordField);
|
||||
}
|
||||
@@ -67,27 +28,17 @@ FloatingWindow {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
function _commitSubmit() {
|
||||
isLoading = true;
|
||||
awaitingFprintForPassword = false;
|
||||
currentFlow.submit(passwordInput);
|
||||
passwordInput = "";
|
||||
}
|
||||
|
||||
function submitAuth() {
|
||||
if (!currentFlow || isLoading)
|
||||
return;
|
||||
if (!currentFlow.isResponseRequired) {
|
||||
awaitingFprintForPassword = true;
|
||||
return;
|
||||
}
|
||||
_commitSubmit();
|
||||
isLoading = true;
|
||||
currentFlow.submit(passwordInput);
|
||||
passwordInput = "";
|
||||
}
|
||||
|
||||
function cancelAuth() {
|
||||
if (isLoading)
|
||||
return;
|
||||
awaitingFprintForPassword = false;
|
||||
if (currentFlow) {
|
||||
currentFlow.cancelAuthenticationRequest();
|
||||
return;
|
||||
@@ -109,7 +60,6 @@ FloatingWindow {
|
||||
}
|
||||
passwordInput = "";
|
||||
isLoading = false;
|
||||
awaitingFprintForPassword = false;
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -133,11 +83,6 @@ FloatingWindow {
|
||||
function onIsResponseRequiredChanged() {
|
||||
if (!currentFlow.isResponseRequired)
|
||||
return;
|
||||
if (awaitingFprintForPassword && passwordInput !== "") {
|
||||
_commitSubmit();
|
||||
return;
|
||||
}
|
||||
awaitingFprintForPassword = false;
|
||||
isLoading = false;
|
||||
passwordInput = "";
|
||||
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 {
|
||||
id: contentFocusScope
|
||||
|
||||
@@ -295,30 +205,36 @@ FloatingWindow {
|
||||
visible: text !== ""
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: passwordField
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: inputFieldHeight
|
||||
backgroundColor: Theme.surfaceHover
|
||||
normalBorderColor: Theme.outlineStrong
|
||||
focusedBorderColor: Theme.primary
|
||||
borderWidth: 1
|
||||
focusedBorderWidth: 2
|
||||
leftIconName: polkitPamHasFprint ? "fingerprint" : ""
|
||||
leftIconSize: 20
|
||||
leftIconColor: Theme.primary
|
||||
leftIconFocusedColor: Theme.primary
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong
|
||||
border.width: passwordField.activeFocus ? 2 : 1
|
||||
opacity: isLoading ? 0.5 : 1
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: Theme.surfaceText
|
||||
text: passwordInput
|
||||
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
|
||||
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
|
||||
placeholderText: ""
|
||||
enabled: !isLoading
|
||||
onTextEdited: passwordInput = text
|
||||
onAccepted: submitAuth()
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: !isLoading
|
||||
onClicked: passwordField.forceActiveFocus()
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
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 {
|
||||
|
||||
@@ -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 {
|
||||
id: pluginsLoader
|
||||
anchors.fill: parent
|
||||
@@ -485,7 +470,7 @@ FocusScope {
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item)
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,22 +485,7 @@ FocusScope {
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item)
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: muxLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 32
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: MuxTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item)
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ Rectangle {
|
||||
{
|
||||
"id": "running_apps",
|
||||
"text": I18n.tr("Running Apps"),
|
||||
"icon": "app_registration",
|
||||
"icon": "apps",
|
||||
"tabIndex": 19,
|
||||
"hyprlandNiriOnly": true
|
||||
},
|
||||
@@ -237,7 +237,7 @@ Rectangle {
|
||||
{
|
||||
"id": "system",
|
||||
"text": I18n.tr("System"),
|
||||
"icon": "memory",
|
||||
"icon": "computer",
|
||||
"collapsedByDefault": true,
|
||||
"children": [
|
||||
{
|
||||
@@ -266,12 +266,6 @@ Rectangle {
|
||||
"tabIndex": 8,
|
||||
"cupsOnly": true
|
||||
},
|
||||
{
|
||||
"id": "multiplexers",
|
||||
"text": I18n.tr("Multiplexers"),
|
||||
"icon": "terminal",
|
||||
"tabIndex": 32
|
||||
},
|
||||
{
|
||||
"id": "window_rules",
|
||||
"text": I18n.tr("Window Rules"),
|
||||
@@ -293,12 +287,6 @@ Rectangle {
|
||||
"icon": "lock",
|
||||
"tabIndex": 11
|
||||
},
|
||||
{
|
||||
"id": "greeter",
|
||||
"text": I18n.tr("Greeter"),
|
||||
"icon": "login",
|
||||
"tabIndex": 31
|
||||
},
|
||||
{
|
||||
"id": "power_sleep",
|
||||
"text": I18n.tr("Power & Sleep"),
|
||||
@@ -370,7 +358,6 @@ Rectangle {
|
||||
if (_collapsedIds.indexOf(marker) < 0)
|
||||
_collapsedIds = _collapsedIds + id + ",";
|
||||
}
|
||||
SessionData.setSettingsSidebarState(_expandedIds, _collapsedIds);
|
||||
}
|
||||
|
||||
function _setAutoExpanded(id, value) {
|
||||
@@ -539,11 +526,6 @@ Rectangle {
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
Component.onCompleted: {
|
||||
root._expandedIds = SessionData.settingsSidebarExpandedIds;
|
||||
root._collapsedIds = SessionData.settingsSidebarCollapsedIds;
|
||||
}
|
||||
|
||||
StyledTextMetrics {
|
||||
id: __m1
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
|
||||
@@ -8,9 +8,6 @@ DankPopout {
|
||||
|
||||
layerNamespace: "dms:app-launcher"
|
||||
|
||||
readonly property real screenWidth: screen?.width ?? 1920
|
||||
readonly property real screenHeight: screen?.height ?? 1080
|
||||
|
||||
property string _pendingMode: ""
|
||||
property string _pendingQuery: ""
|
||||
|
||||
@@ -44,35 +41,8 @@ DankPopout {
|
||||
openWithQuery(query);
|
||||
}
|
||||
|
||||
readonly property int _baseWidth: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
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)
|
||||
|
||||
popupWidth: 560
|
||||
popupHeight: 640
|
||||
triggerWidth: 40
|
||||
positioning: ""
|
||||
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
|
||||
@@ -136,7 +106,7 @@ DankPopout {
|
||||
QtObject {
|
||||
id: modalAdapter
|
||||
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
||||
property bool isClosing: false
|
||||
property bool isClosing: appDrawerPopout.isClosing
|
||||
|
||||
function hide() {
|
||||
appDrawerPopout.close();
|
||||
|
||||
@@ -86,7 +86,7 @@ Variants {
|
||||
|
||||
Component.onCompleted: {
|
||||
if (typeof blurWallpaperWindow.updatesEnabled !== "undefined")
|
||||
blurWallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
|
||||
blurWallpaperWindow.updatesEnabled = Qt.binding(() => root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
@@ -100,10 +100,10 @@ Variants {
|
||||
Connections {
|
||||
target: currentWallpaper
|
||||
function onStatusChanged() {
|
||||
if (currentWallpaper.status !== Image.Ready && currentWallpaper.status !== Image.Error)
|
||||
return;
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
if (currentWallpaper.status === Image.Ready) {
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,8 +167,6 @@ Variants {
|
||||
transitionAnimation.stop();
|
||||
root.transitionProgress = 0;
|
||||
root.effectActive = false;
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
currentWallpaper.source = nextWallpaper.source;
|
||||
nextWallpaper.source = "";
|
||||
}
|
||||
@@ -177,9 +175,6 @@ Variants {
|
||||
return;
|
||||
}
|
||||
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
|
||||
nextWallpaper.source = newPath;
|
||||
|
||||
if (nextWallpaper.status === Image.Ready)
|
||||
@@ -206,7 +201,6 @@ Variants {
|
||||
visible: false
|
||||
opacity: 1
|
||||
asynchronous: true
|
||||
retainWhileLoading: true
|
||||
smooth: true
|
||||
cache: true
|
||||
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
||||
@@ -219,7 +213,6 @@ Variants {
|
||||
visible: false
|
||||
opacity: 0
|
||||
asynchronous: true
|
||||
retainWhileLoading: true
|
||||
smooth: true
|
||||
cache: true
|
||||
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
||||
@@ -302,8 +295,6 @@ Variants {
|
||||
root.useNextForEffect = false;
|
||||
nextWallpaper.source = "";
|
||||
root.transitionProgress = 0.0;
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
root.effectActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ DankPopout {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:control-center"
|
||||
fullHeightSurface: true
|
||||
fullHeightSurface: false
|
||||
|
||||
property string expandedSection: ""
|
||||
property var triggerScreen: null
|
||||
@@ -70,16 +70,6 @@ DankPopout {
|
||||
|
||||
backgroundInteractive: !anyModalOpen
|
||||
|
||||
onCredentialsPromptOpenChanged: {
|
||||
if (credentialsPromptOpen && shouldBeVisible)
|
||||
close();
|
||||
}
|
||||
|
||||
onPolkitModalOpenChanged: {
|
||||
if (polkitModalOpen && shouldBeVisible)
|
||||
close();
|
||||
}
|
||||
|
||||
customKeyboardFocus: {
|
||||
if (!shouldBeVisible)
|
||||
return WlrKeyboardFocus.None;
|
||||
@@ -136,9 +126,11 @@ DankPopout {
|
||||
z: 5000
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1117,6 +1117,7 @@ Item {
|
||||
if (!notificationCenterLoader.item) {
|
||||
return;
|
||||
}
|
||||
notificationCenterLoader.item.triggerScreen = barWindow.screen;
|
||||
const effectiveBarConfig = topBarContent.barConfig;
|
||||
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
|
||||
if (notificationCenterLoader.item.setBarContext) {
|
||||
|
||||
@@ -289,7 +289,7 @@ PanelWindow {
|
||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||
if (!onThisScreen)
|
||||
return false;
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
@@ -312,7 +312,7 @@ PanelWindow {
|
||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||
if (!onThisScreen)
|
||||
return false;
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
@@ -336,7 +336,7 @@ PanelWindow {
|
||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||
if (!onThisScreen)
|
||||
return false;
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
@@ -360,7 +360,7 @@ PanelWindow {
|
||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||
if (!onThisScreen)
|
||||
return false;
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
@@ -686,7 +686,6 @@ PanelWindow {
|
||||
onHasActivePopoutChanged: evaluateReveal()
|
||||
|
||||
function updateActivePopoutState() {
|
||||
if (!barWindow.screen) return;
|
||||
const screenName = barWindow.screen.name;
|
||||
const activePopout = PopoutManager.currentPopoutsByScreen[screenName];
|
||||
const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName];
|
||||
|
||||
@@ -22,12 +22,12 @@ DankPopout {
|
||||
|
||||
function setProfile(profile) {
|
||||
if (typeof PowerProfiles === "undefined") {
|
||||
ToastService.showError(I18n.tr("power-profiles-daemon not available"));
|
||||
ToastService.showError("power-profiles-daemon not available");
|
||||
return;
|
||||
}
|
||||
PowerProfiles.profile = profile;
|
||||
if (PowerProfiles.profile !== profile) {
|
||||
ToastService.showError(I18n.tr("Failed to set power profile"));
|
||||
ToastService.showError("Failed to set power profile");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,10 +173,10 @@ DankPopout {
|
||||
width: parent.width - Theme.iconSizeLarge - 32 - Theme.spacingM * 2
|
||||
readonly property string timeInfoText: {
|
||||
if (!BatteryService.batteryAvailable)
|
||||
return I18n.tr("Power profile management available");
|
||||
return "Power profile management available";
|
||||
const time = BatteryService.formatTimeRemaining();
|
||||
if (time !== "Unknown") {
|
||||
return BatteryService.isCharging ? I18n.tr("Time until full: %1").arg(time) : I18n.tr("Time remaining: %1").arg(time);
|
||||
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -188,7 +188,7 @@ DankPopout {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : I18n.tr("Power")
|
||||
text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : "Power"
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: {
|
||||
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
|
||||
@@ -338,7 +338,7 @@ DankPopout {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: BatteryService.batteryCapacity > 0 ? `${BatteryService.batteryCapacity.toFixed(1)} Wh` : I18n.tr("Unknown")
|
||||
text: BatteryService.batteryCapacity > 0 ? `${BatteryService.batteryCapacity.toFixed(1)} Wh` : "Unknown"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Bold
|
||||
@@ -393,7 +393,7 @@ DankPopout {
|
||||
width: parent.width - percentText.width - chargingIcon.width - Theme.spacingM * 2
|
||||
|
||||
StyledText {
|
||||
text: modelData.model || I18n.tr("Battery %1").arg(index + 1)
|
||||
text: modelData.model || `Battery ${index + 1}`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
|
||||
@@ -248,7 +248,7 @@ BasePill {
|
||||
let appId = Paths.moddedAppId(rawAppId);
|
||||
|
||||
let coreAppData = null;
|
||||
if (rawAppId === "org.quickshell" || rawAppId === "com.danklinux.dms") {
|
||||
if (rawAppId === "org.quickshell") {
|
||||
coreAppData = getCoreAppDataByTitle(toplevel.title);
|
||||
if (coreAppData) {
|
||||
appId = coreAppData.builtInPluginId;
|
||||
@@ -697,7 +697,7 @@ BasePill {
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready && !coreIcon.visible
|
||||
layer.enabled: appItem.appId === "org.quickshell" || appItem.appId === "com.danklinux.dms"
|
||||
layer.enabled: appItem.appId === "org.quickshell"
|
||||
layer.smooth: true
|
||||
layer.mipmap: true
|
||||
layer.effect: MultiEffect {
|
||||
@@ -990,7 +990,7 @@ BasePill {
|
||||
break;
|
||||
}
|
||||
|
||||
const shouldHidePin = modelData.appId === "org.quickshell" || modelData.appId === "com.danklinux.dms";
|
||||
const shouldHidePin = modelData.appId === "org.quickshell";
|
||||
const moddedId = Paths.moddedAppId(modelData.appId);
|
||||
const desktopEntry = moddedId ? DesktopEntries.heuristicLookup(moddedId) : null;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ PanelWindow {
|
||||
property int margin: 10
|
||||
property bool hidePin: false
|
||||
property var desktopEntry: null
|
||||
property bool isDmsWindow: appData?.appId === "org.quickshell" || appData?.appId === "com.danklinux.dms"
|
||||
property bool isDmsWindow: appData?.appId === "org.quickshell"
|
||||
|
||||
property bool isVertical: false
|
||||
property string edge: "top"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
@@ -39,20 +38,12 @@ BasePill {
|
||||
property var _vAudio: null
|
||||
property var _vBrightness: 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) {
|
||||
const delta = wheelEvent.angleDelta.y;
|
||||
if (delta === 0)
|
||||
return;
|
||||
|
||||
root.refreshInteractionRefs();
|
||||
|
||||
const rootX = wheelEvent.x - root.leftMargin;
|
||||
const rootY = wheelEvent.y - root.topMargin;
|
||||
|
||||
@@ -81,8 +72,6 @@ BasePill {
|
||||
}
|
||||
|
||||
onRightClicked: function (rootX, rootY) {
|
||||
root.refreshInteractionRefs();
|
||||
|
||||
if (root.isVerticalOrientation && _vCol) {
|
||||
const pos = root.mapToItem(_vCol, rootX, rootY);
|
||||
if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) {
|
||||
@@ -290,142 +279,26 @@ BasePill {
|
||||
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() {
|
||||
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 {
|
||||
@@ -436,7 +309,12 @@ BasePill {
|
||||
Component.onCompleted: {
|
||||
root._hRow = controlIndicators;
|
||||
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 {
|
||||
@@ -446,151 +324,159 @@ BasePill {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: root.controlCenterRenderModel
|
||||
Item {
|
||||
id: verticalGroupItem
|
||||
required property var modelData
|
||||
required property int index
|
||||
property string interactionGroupId: modelData.id
|
||||
Item {
|
||||
width: parent.width
|
||||
height: root.vIconSize
|
||||
visible: root.showScreenSharingIcon && NiriService.hasCasts
|
||||
|
||||
width: parent.width
|
||||
height: {
|
||||
switch (modelData.id) {
|
||||
case "audio":
|
||||
return root.vIconSize + (audioPercentV.visible ? audioPercentV.implicitHeight + 2 : 0);
|
||||
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
|
||||
DankIcon {
|
||||
name: "screen_record"
|
||||
size: root.vIconSize
|
||||
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.registerInteractionDelegate(true, verticalGroupItem);
|
||||
root.refreshInteractionRefs();
|
||||
}
|
||||
Component.onDestruction: {
|
||||
if (root) {
|
||||
root.unregisterInteractionDelegate(verticalGroupItem);
|
||||
root.refreshInteractionRefs();
|
||||
}
|
||||
}
|
||||
onVisibleChanged: root.refreshInteractionRefs()
|
||||
onInteractionGroupIdChanged: {
|
||||
root.refreshInteractionRefs();
|
||||
}
|
||||
Item {
|
||||
width: parent.width
|
||||
height: root.vIconSize
|
||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: !verticalGroupItem.modelData.composite
|
||||
name: {
|
||||
switch (verticalGroupItem.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.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 {
|
||||
name: root.getNetworkIconName()
|
||||
size: root.vIconSize
|
||||
color: root.getNetworkIconColor()
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: audioIconV
|
||||
visible: verticalGroupItem.modelData.id === "audio"
|
||||
name: root.getVolumeIconName()
|
||||
size: root.vIconSize
|
||||
color: Theme.widgetIconColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
}
|
||||
Item {
|
||||
width: parent.width
|
||||
height: root.vIconSize
|
||||
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
|
||||
|
||||
NumericText {
|
||||
id: audioPercentV
|
||||
visible: verticalGroupItem.modelData.id === "audio" && root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume)
|
||||
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
||||
reserveText: "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
|
||||
}
|
||||
DankIcon {
|
||||
name: "vpn_lock"
|
||||
size: root.vIconSize
|
||||
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: micIconV
|
||||
visible: verticalGroupItem.modelData.id === "microphone"
|
||||
name: root.getMicIconName()
|
||||
size: root.vIconSize
|
||||
color: root.getMicIconColor()
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
}
|
||||
Item {
|
||||
width: parent.width
|
||||
height: root.vIconSize
|
||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
||||
|
||||
NumericText {
|
||||
id: micPercentV
|
||||
visible: verticalGroupItem.modelData.id === "microphone" && root.showMicPercent && isFinite(AudioService.source?.audio?.volume)
|
||||
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
||||
reserveText: "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
|
||||
}
|
||||
DankIcon {
|
||||
name: "bluetooth"
|
||||
size: root.vIconSize
|
||||
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: brightnessIconV
|
||||
visible: verticalGroupItem.modelData.id === "brightness"
|
||||
name: root.getBrightnessIconName()
|
||||
size: root.vIconSize
|
||||
color: Theme.widgetIconColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
}
|
||||
Item {
|
||||
width: parent.width
|
||||
height: root.vIconSize + (root.showAudioPercent ? audioPercentV.implicitHeight + 2 : 0)
|
||||
visible: root.showAudioIcon
|
||||
|
||||
NumericText {
|
||||
id: brightnessPercentV
|
||||
visible: verticalGroupItem.modelData.id === "brightness" && 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.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: brightnessIconV.bottom
|
||||
anchors.topMargin: 2
|
||||
}
|
||||
DankIcon {
|
||||
id: audioIconV
|
||||
name: root.getVolumeIconName()
|
||||
size: root.vIconSize
|
||||
color: Theme.widgetIconColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
}
|
||||
|
||||
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
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: root.controlCenterRenderModel
|
||||
DankIcon {
|
||||
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 {
|
||||
id: horizontalGroupItem
|
||||
required property var modelData
|
||||
required property int index
|
||||
property string interactionGroupId: modelData.id
|
||||
DankIcon {
|
||||
id: networkIcon
|
||||
name: root.getNetworkIconName()
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: root.getNetworkIconColor()
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
||||
}
|
||||
|
||||
width: {
|
||||
switch (modelData.id) {
|
||||
case "audio":
|
||||
return audioGroup.width;
|
||||
case "microphone":
|
||||
return micGroup.width;
|
||||
case "brightness":
|
||||
return brightnessGroup.width;
|
||||
default:
|
||||
return root.getControlCenterIconSize();
|
||||
}
|
||||
}
|
||||
implicitWidth: width
|
||||
height: root.widgetThickness - root.horizontalPadding * 2
|
||||
visible: modelData.visible
|
||||
DankIcon {
|
||||
id: vpnIcon
|
||||
name: "vpn_lock"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.registerInteractionDelegate(false, horizontalGroupItem);
|
||||
root.refreshInteractionRefs();
|
||||
}
|
||||
Component.onDestruction: {
|
||||
if (root) {
|
||||
root.unregisterInteractionDelegate(horizontalGroupItem);
|
||||
root.refreshInteractionRefs();
|
||||
}
|
||||
}
|
||||
onVisibleChanged: root.refreshInteractionRefs()
|
||||
onInteractionGroupIdChanged: {
|
||||
root.refreshInteractionRefs();
|
||||
}
|
||||
DankIcon {
|
||||
id: bluetoothIcon
|
||||
name: "bluetooth"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: iconOnlyItem
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
visible: !horizontalGroupItem.modelData.composite
|
||||
name: {
|
||||
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 {
|
||||
width: audioIcon.implicitWidth + (root.showAudioPercent ? audioPercent.implicitWidth : 0) + 4
|
||||
height: root.widgetThickness - root.horizontalPadding * 2
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showAudioIcon
|
||||
|
||||
Rectangle {
|
||||
id: audioGroup
|
||||
width: audioContent.implicitWidth + 2
|
||||
implicitWidth: width
|
||||
height: parent.height
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: horizontalGroupItem.modelData.id === "audio"
|
||||
DankIcon {
|
||||
id: audioIcon
|
||||
name: root.getVolumeIconName()
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: Theme.widgetIconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 2
|
||||
}
|
||||
|
||||
Row {
|
||||
id: audioContent
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 1
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
StyledText {
|
||||
id: audioPercent
|
||||
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.verticalCenter: parent.verticalCenter
|
||||
anchors.left: audioIcon.right
|
||||
anchors.leftMargin: 2
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: audioIcon
|
||||
name: root.getVolumeIconName()
|
||||
size: root.getControlCenterIconSize()
|
||||
color: Theme.widgetIconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Rectangle {
|
||||
width: micIcon.implicitWidth + (root.showMicPercent ? micPercent.implicitWidth : 0) + 4
|
||||
height: root.widgetThickness - root.horizontalPadding * 2
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showMicIcon
|
||||
|
||||
NumericText {
|
||||
id: audioPercent
|
||||
visible: root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume)
|
||||
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 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: micIcon
|
||||
name: root.getMicIconName()
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: root.getMicIconColor()
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: micGroup
|
||||
width: micContent.implicitWidth + 2
|
||||
implicitWidth: width
|
||||
height: parent.height
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: horizontalGroupItem.modelData.id === "microphone"
|
||||
StyledText {
|
||||
id: micPercent
|
||||
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.verticalCenter: parent.verticalCenter
|
||||
anchors.left: micIcon.right
|
||||
anchors.leftMargin: 2
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: micContent
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 1
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
Rectangle {
|
||||
width: brightnessIcon.implicitWidth + (root.showBrightnessPercent ? brightnessPercent.implicitWidth : 0) + 4
|
||||
height: root.widgetThickness - root.horizontalPadding * 2
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
|
||||
|
||||
DankIcon {
|
||||
id: micIcon
|
||||
name: root.getMicIconName()
|
||||
size: root.getControlCenterIconSize()
|
||||
color: root.getMicIconColor()
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
DankIcon {
|
||||
id: brightnessIcon
|
||||
name: root.getBrightnessIconName()
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: Theme.widgetIconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 2
|
||||
}
|
||||
|
||||
NumericText {
|
||||
id: micPercent
|
||||
visible: root.showMicPercent && isFinite(AudioService.source?.audio?.volume)
|
||||
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
id: brightnessPercent
|
||||
visible: root.showBrightnessPercent
|
||||
text: Math.round(getBrightness() * 100) + "%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: brightnessIcon.right
|
||||
anchors.leftMargin: 2
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
name: "settings"
|
||||
size: root.getControlCenterIconSize()
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: root.isActive ? Theme.primary : Theme.widgetIconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.hasNoVisibleIcons()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user