mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-14 00:02:45 -04:00
Compare commits
47 Commits
anims
...
e78ba77def
| Author | SHA1 | Date | |
|---|---|---|---|
| e78ba77def | |||
| 7113afe9e2 | |||
| 1a2b6524e6 | |||
| 95c4aa9e4c | |||
| 9f2518c9e1 | |||
| 76c50a654a | |||
| ded2c38551 | |||
| 772094eacd | |||
| bddc2f6295 | |||
| 25dce2961b | |||
| 653cfbe6e0 | |||
| c539311083 | |||
| 60118c5d5b | |||
| c6b9b36566 | |||
| fd5b1b7c00 | |||
| ebc77b62c8 | |||
| 2ce888581f | |||
| 0e901b6404 | |||
| 688b9076e7 | |||
| c6ec7579b6 | |||
| 9417edac8d | |||
| 6185cc79d7 | |||
| 4ecdba94c2 | |||
| a11640d840 | |||
| 177a4c4095 | |||
| 63df19ab78 | |||
| 54e0eb5979 | |||
| 185284d422 | |||
| ce240405d9 | |||
| 58b700ed0d | |||
| d436fa4920 | |||
| d58486193e | |||
| e9404eb9b6 | |||
| 0fef4d515e | |||
| 86f9cf4376 | |||
| acf63c57e8 | |||
| baa956c3a1 | |||
| bb2081a936 | |||
| c984b0b9ae | |||
| 754bf8fa3c | |||
| 7840294517 | |||
| caaee88654 | |||
| e872ddc1e7 | |||
| 1eca9b4c2c | |||
| fe5bd42e25 | |||
| 32d16d0673 | |||
| 27c26d35ab |
@@ -20,7 +20,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: core/go.mod
|
go-version-file: core/go.mod
|
||||||
|
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifacts (${{ matrix.arch }})
|
- name: Upload artifacts (${{ matrix.arch }})
|
||||||
if: matrix.arch == 'arm64'
|
if: matrix.arch == 'arm64'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifacts with completions
|
- name: Upload artifacts with completions
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -147,7 +147,7 @@ jobs:
|
|||||||
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
# - name: Checkout
|
# - name: Checkout
|
||||||
# uses: actions/checkout@v4
|
# uses: actions/checkout@v6
|
||||||
# with:
|
# with:
|
||||||
# token: ${{ steps.app_token.outputs.token }}
|
# token: ${{ steps.app_token.outputs.token }}
|
||||||
# fetch-depth: 0
|
# fetch-depth: 0
|
||||||
@@ -181,7 +181,7 @@ jobs:
|
|||||||
TAG: ${{ inputs.tag }}
|
TAG: ${{ inputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -192,12 +192,12 @@ jobs:
|
|||||||
git checkout ${TAG}
|
git checkout ${TAG}
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Download core artifacts
|
- name: Download core artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: core-assets-*
|
pattern: core-assets-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Determine version
|
- name: Determine version
|
||||||
id: version
|
id: version
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
rpm -qpi "$SRPM"
|
rpm -qpi "$SRPM"
|
||||||
|
|
||||||
- name: Upload SRPM artifact
|
- name: Upload SRPM artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
||||||
path: ${{ steps.build.outputs.srpm_path }}
|
path: ${{ steps.build.outputs.srpm_path }}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -195,10 +195,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Wait before OBS upload
|
||||||
|
run: sleep 3
|
||||||
|
|
||||||
- name: Determine packages to update
|
- name: Determine packages to update
|
||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
@@ -344,7 +347,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -157,12 +157,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
cache: false
|
cache: false
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|||||||
+3
-1
@@ -86,7 +86,9 @@ touch .qmlls.ini
|
|||||||
|
|
||||||
4. Restart dms to generate the `.qmlls.ini` file
|
4. Restart dms to generate the `.qmlls.ini` file
|
||||||
|
|
||||||
5. Make your changes, test, and open a pull request.
|
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.
|
||||||
|
|
||||||
### I18n/Localization
|
### I18n/Localization
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
|||||||
ASSETS_DIR=assets
|
ASSETS_DIR=assets
|
||||||
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
||||||
|
|
||||||
.PHONY: all build clean 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 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
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@@ -32,6 +32,9 @@ clean:
|
|||||||
@$(MAKE) -C $(CORE_DIR) clean
|
@$(MAKE) -C $(CORE_DIR) clean
|
||||||
@echo "Clean complete"
|
@echo "Clean complete"
|
||||||
|
|
||||||
|
lint-qml:
|
||||||
|
@./quickshell/scripts/qmllint-entrypoints.sh
|
||||||
|
|
||||||
# Installation targets
|
# Installation targets
|
||||||
install-bin:
|
install-bin:
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@@ -130,6 +133,7 @@ help:
|
|||||||
@echo " all (default) - Build the DMS binary"
|
@echo " all (default) - Build the DMS binary"
|
||||||
@echo " build - Same as 'all'"
|
@echo " build - Same as 'all'"
|
||||||
@echo " clean - Clean build artifacts"
|
@echo " clean - Clean build artifacts"
|
||||||
|
@echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Install:"
|
@echo "Install:"
|
||||||
@echo " install - Build and install everything (requires sudo)"
|
@echo " install - Build and install everything (requires sudo)"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"policy_version": 1,
|
||||||
|
"blocked_commands": [
|
||||||
|
"greeter install",
|
||||||
|
"greeter enable",
|
||||||
|
"greeter sync",
|
||||||
|
"greeter uninstall",
|
||||||
|
"setup"
|
||||||
|
],
|
||||||
|
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
|
||||||
|
}
|
||||||
@@ -222,16 +222,19 @@ func init() {
|
|||||||
|
|
||||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||||
var data []byte
|
var data []byte
|
||||||
|
copyFromStdin := false
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case len(args) > 0:
|
case len(args) > 0:
|
||||||
data = []byte(args[0])
|
data = []byte(args[0])
|
||||||
default:
|
case clipCopyDownload || clipCopyType == "__multi__":
|
||||||
var err error
|
var err error
|
||||||
data, err = io.ReadAll(os.Stdin)
|
data, err = io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("read stdin: %v", err)
|
log.Fatalf("read stdin: %v", err)
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
copyFromStdin = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if clipCopyDownload {
|
if clipCopyDownload {
|
||||||
@@ -257,6 +260,13 @@ func runClipCopy(cmd *cobra.Command, args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if copyFromStdin {
|
||||||
|
if err := clipboard.CopyReader(os.Stdin, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
||||||
|
log.Fatalf("copy: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
||||||
log.Fatalf("copy: %v", err)
|
log.Fatalf("copy: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+1097
-157
File diff suppressed because it is too large
Load Diff
@@ -16,9 +16,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var setupCmd = &cobra.Command{
|
var setupCmd = &cobra.Command{
|
||||||
Use: "setup",
|
Use: "setup",
|
||||||
Short: "Deploy DMS configurations",
|
Short: "Deploy DMS configurations",
|
||||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||||
|
PersistentPreRunE: requireMutableSystemCommand,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := runSetup(); err != nil {
|
if err := runSetup(); err != nil {
|
||||||
log.Fatalf("Error during setup: %v", err)
|
log.Fatalf("Error during setup: %v", err)
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
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", "greeter sync", "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)
|
||||||
|
}
|
||||||
+1
-10
@@ -16,19 +16,10 @@ func init() {
|
|||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
// Add subcommands to greeter
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
|
||||||
|
|
||||||
// Add subcommands to setup
|
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
|
|
||||||
// Add subcommands to update
|
|
||||||
updateCmd.AddCommand(updateCheckCmd)
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
|
|
||||||
// Add subcommands to plugins
|
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
// Add common commands to root
|
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
rootCmd.AddCommand(updateCmd)
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
|||||||
@@ -11,29 +11,20 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Add flags
|
|
||||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
||||||
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
// Add subcommands to greeter
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
|
||||||
|
|
||||||
// Add subcommands to setup
|
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
|
|
||||||
// Add subcommands to plugins
|
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
// Add common commands to root
|
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Block root
|
|
||||||
if os.Geteuid() == 0 {
|
if os.Geteuid() == 0 {
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func findCommandPath(cmd string) (string, error) {
|
|
||||||
path, err := exec.LookPath(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
|
|
||||||
}
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isArchPackageInstalled(packageName string) bool {
|
func isArchPackageInstalled(packageName string) bool {
|
||||||
cmd := exec.Command("pacman", "-Q", packageName)
|
cmd := exec.Command("pacman", "-Q", packageName)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package clipboard
|
package clipboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||||
@@ -12,17 +14,37 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Copy(data []byte, mimeType string) error {
|
func Copy(data []byte, mimeType string) error {
|
||||||
return CopyOpts(data, mimeType, false, false)
|
return CopyReader(bytes.NewReader(data), mimeType, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||||
|
if foreground {
|
||||||
|
return copyServeWithWriter(func(writer io.Writer) error {
|
||||||
|
total := 0
|
||||||
|
for total < len(data) {
|
||||||
|
n, err := writer.Write(data[total:])
|
||||||
|
total += n
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if total != len(data) {
|
||||||
|
return io.ErrShortWrite
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, mimeType, pasteOnce)
|
||||||
|
}
|
||||||
|
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if !foreground {
|
if !foreground {
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
return copyServe(data, mimeType, pasteOnce)
|
return copyServeReader(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
||||||
if pasteOnce {
|
if pasteOnce {
|
||||||
args = append(args, "--paste-once")
|
args = append(args, "--paste-once")
|
||||||
@@ -30,11 +52,15 @@ func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
args = append(args, "--type", mimeType)
|
args = append(args, "--type", mimeType)
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
cmd.Stdin = nil
|
|
||||||
cmd.Stdout = nil
|
cmd.Stdout = nil
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
|
||||||
|
if stdinSource, ok := data.(*os.File); ok {
|
||||||
|
cmd.Stdin = stdinSource
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
|
|
||||||
stdin, err := cmd.StdinPipe()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
@@ -44,16 +70,66 @@ func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
return fmt.Errorf("start: %w", err)
|
return fmt.Errorf("start: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := stdin.Write(data); err != nil {
|
if _, err := io.Copy(stdin, data); err != nil {
|
||||||
stdin.Close()
|
stdin.Close()
|
||||||
return fmt.Errorf("write stdin: %w", err)
|
return fmt.Errorf("write stdin: %w", err)
|
||||||
}
|
}
|
||||||
stdin.Close()
|
if err := stdin.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close stdin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
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 {
|
||||||
display, err := wlclient.Connect("")
|
display, err := wlclient.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
return fmt.Errorf("wayland connect: %w", err)
|
||||||
@@ -139,12 +215,18 @@ func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
|
|
||||||
cancelled := make(chan struct{})
|
cancelled := make(chan struct{})
|
||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
|
sendErr := make(chan error, 1)
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
defer syscall.Close(e.Fd)
|
defer syscall.Close(e.Fd)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
file.Write(data)
|
if err := writeTo(file); err != nil {
|
||||||
|
select {
|
||||||
|
case sendErr <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
select {
|
select {
|
||||||
case pasted <- struct{}{}:
|
case pasted <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -165,6 +247,8 @@ func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
select {
|
select {
|
||||||
case <-cancelled:
|
case <-cancelled:
|
||||||
return nil
|
return nil
|
||||||
|
case err := <-sendErr:
|
||||||
|
return err
|
||||||
case <-pasted:
|
case <-pasted:
|
||||||
if pasteOnce {
|
if pasteOnce {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -440,29 +440,10 @@ func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []st
|
|||||||
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
|
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
hasNiri := false
|
hasNiri := false
|
||||||
hasQuickshell := false
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
if pkg == "niri-git" {
|
if pkg == "niri-git" {
|
||||||
hasNiri = true
|
hasNiri = true
|
||||||
}
|
}
|
||||||
if pkg == "quickshell" || pkg == "quickshell-git" {
|
|
||||||
hasQuickshell = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If quickshell is in the list, always reinstall google-breakpad first
|
|
||||||
if hasQuickshell {
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: 0.63,
|
|
||||||
Step: "Reinstalling google-breakpad for quickshell...",
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
|
|
||||||
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
|
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
|
||||||
@@ -616,10 +597,16 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
|
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip dependency installation for dms-shell-git and dms-shell-bin
|
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||||
// since we manually manage those dependencies
|
if pkg == "dms-shell-bin" {
|
||||||
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
|
progressChan <- InstallProgressMsg{
|
||||||
// Pre-install dependencies from .SRCINFO
|
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 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: PhaseAURPackages,
|
||||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||||
@@ -628,19 +615,19 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
CommandInfo: "Installing package dependencies and makedepends",
|
CommandInfo: "Installing package dependencies and makedepends",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install dependencies and makedepends explicitly
|
// Install dependencies from .SRCINFO
|
||||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
depFilter := ""
|
||||||
|
if pkg == "dms-shell-git" {
|
||||||
|
depFilter = ` | sed -E 's/[[:space:]]*(quickshell|dgop)[[:space:]]*/ /g' | tr -s ' '`
|
||||||
|
}
|
||||||
|
|
||||||
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||||
fmt.Sprintf(`
|
fmt.Sprintf(`
|
||||||
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' %s | sed 's/[[:space:]]*$//')
|
||||||
if [[ "%s" == *"quickshell"* ]]; then
|
|
||||||
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
|
|
||||||
fi
|
|
||||||
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
||||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
||||||
fi
|
fi
|
||||||
`, srcinfoPath, pkg, sudoPassword))
|
`, srcinfoPath, depFilter, sudoPassword))
|
||||||
|
|
||||||
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
|
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
|
||||||
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
|
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
|
||||||
@@ -657,14 +644,6 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
|
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
|
||||||
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
|
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: startProgress + 0.35*(endProgress-startProgress),
|
|
||||||
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
|
|
||||||
IsComplete: false,
|
|
||||||
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -677,7 +656,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
|
|
||||||
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
||||||
buildCmd.Dir = packageDir
|
buildCmd.Dir = packageDir
|
||||||
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed
|
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar")
|
||||||
|
|
||||||
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
|
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
|
||||||
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# 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"),
|
||||||
|
}
|
||||||
+1105
-108
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,7 @@ var templateRegistry = []TemplateDef{
|
|||||||
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
||||||
{ID: "vscode", Kind: TemplateKindVSCode},
|
{ID: "vscode", Kind: TemplateKindVSCode},
|
||||||
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
|
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
|
||||||
|
{ID: "zed", Commands: []string{"zed"}, ConfigFile: "zed.toml"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ColorMode) GTKTheme() string {
|
func (c *ColorMode) GTKTheme() string {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -28,7 +29,13 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
|
|||||||
|
|
||||||
func TestDetectNetworkStack_Integration(t *testing.T) {
|
func TestDetectNetworkStack_Integration(t *testing.T) {
|
||||||
result, err := DetectNetworkStack()
|
result, err := DetectNetworkStack()
|
||||||
|
|
||||||
|
if err != nil && strings.Contains(err.Error(), "connect system bus") {
|
||||||
|
t.Skipf("system D-Bus unavailable: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, result)
|
if assert.NotNil(t, result) {
|
||||||
assert.NotEmpty(t, result.ChosenReason)
|
assert.NotEmpty(t, result.ChosenReason)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,19 @@ if [[ ! -d "distro/debian" ]]; then
|
|||||||
echo "Error: Run this script from the repository root"
|
echo "Error: Run this script from the repository root"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Retry wrapper for osc commands (mitigates SSL "Connection reset by peer" from api.opensuse.org)
|
||||||
|
osc_retry() {
|
||||||
|
local max=3 attempt=1
|
||||||
|
while true; do
|
||||||
|
if osc "$@"; then return 0; fi
|
||||||
|
((attempt >= max)) && return 1
|
||||||
|
echo "Retrying in $((5*attempt))s (attempt $attempt/$max)..."
|
||||||
|
sleep $((5*attempt))
|
||||||
|
((attempt++))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# Parameters:
|
# Parameters:
|
||||||
# $1 = PROJECT
|
# $1 = PROJECT
|
||||||
# $2 = PACKAGE
|
# $2 = PACKAGE
|
||||||
@@ -309,8 +322,23 @@ mkdir -p "$OBS_BASE"
|
|||||||
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
|
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
|
||||||
echo "Checking out $OBS_PROJECT/$PACKAGE..."
|
echo "Checking out $OBS_PROJECT/$PACKAGE..."
|
||||||
cd "$OBS_BASE"
|
cd "$OBS_BASE"
|
||||||
osc co "$OBS_PROJECT/$PACKAGE"
|
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
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
|
if [[ "$CHECKOUT_OK" != "true" ]]; then
|
||||||
|
echo "Error: Checkout failed after 3 attempts"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE"
|
WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE"
|
||||||
@@ -1064,7 +1092,7 @@ fi
|
|||||||
|
|
||||||
# Update working copy to latest revision (without expanding service files to avoid revision conflicts)
|
# Update working copy to latest revision (without expanding service files to avoid revision conflicts)
|
||||||
echo "==> Updating working copy"
|
echo "==> Updating working copy"
|
||||||
if ! osc up 2>/dev/null; then
|
if ! osc_retry up 2>/dev/null; then
|
||||||
echo "Error: Failed to update working copy"
|
echo "Error: Failed to update working copy"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -1145,7 +1173,7 @@ if ! osc status 2>/dev/null | grep -qE '^[MAD]|^[?]'; then
|
|||||||
else
|
else
|
||||||
echo "==> Committing to OBS"
|
echo "==> Committing to OBS"
|
||||||
set +e
|
set +e
|
||||||
osc commit --skip-local-service-run -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs"
|
osc_retry commit --skip-local-service-run -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs"
|
||||||
COMMIT_EXIT=${PIPESTATUS[0]}
|
COMMIT_EXIT=${PIPESTATUS[0]}
|
||||||
set -e
|
set -e
|
||||||
if [[ $COMMIT_EXIT -ne 0 ]]; then
|
if [[ $COMMIT_EXIT -ne 0 ]]; then
|
||||||
|
|||||||
@@ -72,76 +72,82 @@
|
|||||||
"${cleanVersion}${dateSuffix}${revSuffix}";
|
"${cleanVersion}${dateSuffix}${revSuffix}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
dms-shell = pkgs.buildGoModule (
|
dms-shell = pkgs.lib.makeOverridable (
|
||||||
let
|
|
||||||
rootSrc = ./.;
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
inherit version;
|
extraQtPackages ? [ ],
|
||||||
pname = "dms-shell";
|
}:
|
||||||
src = ./core;
|
pkgs.buildGoModule (
|
||||||
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
|
let
|
||||||
|
rootSrc = ./.;
|
||||||
|
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit version;
|
||||||
|
pname = "dms-shell";
|
||||||
|
src = ./core;
|
||||||
|
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
|
||||||
|
|
||||||
subPackages = [ "cmd/dms" ];
|
subPackages = [ "cmd/dms" ];
|
||||||
|
|
||||||
ldflags = [
|
ldflags = [
|
||||||
"-s"
|
"-s"
|
||||||
"-w"
|
"-w"
|
||||||
"-X 'main.Version=${version}'"
|
"-X 'main.Version=${version}'"
|
||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
installShellFiles
|
installShellFiles
|
||||||
makeWrapper
|
makeWrapper
|
||||||
];
|
];
|
||||||
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
mkdir -p $out/share/quickshell/dms
|
mkdir -p $out/share/quickshell/dms
|
||||||
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
||||||
|
|
||||||
chmod u+w $out/share/quickshell/dms/VERSION
|
chmod u+w $out/share/quickshell/dms/VERSION
|
||||||
echo "${version}" > $out/share/quickshell/dms/VERSION
|
echo "${version}" > $out/share/quickshell/dms/VERSION
|
||||||
|
|
||||||
# Install desktop file and icon
|
# Install desktop file and icon
|
||||||
install -D ${rootSrc}/assets/dms-open.desktop \
|
install -D ${rootSrc}/assets/dms-open.desktop \
|
||||||
$out/share/applications/dms-open.desktop
|
$out/share/applications/dms-open.desktop
|
||||||
install -D ${rootSrc}/core/assets/danklogo.svg \
|
install -D ${rootSrc}/core/assets/danklogo.svg \
|
||||||
$out/share/hicolor/scalable/apps/danklogo.svg
|
$out/share/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
wrapProgram $out/bin/dms \
|
wrapProgram $out/bin/dms \
|
||||||
--add-flags "-c $out/share/quickshell/dms" \
|
--add-flags "-c $out/share/quickshell/dms" \
|
||||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}" \
|
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
|
||||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs (qmlPkgs pkgs)}"
|
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
|
||||||
|
|
||||||
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
||||||
$out/lib/systemd/user/dms.service
|
$out/lib/systemd/user/dms.service
|
||||||
|
|
||||||
substituteInPlace $out/lib/systemd/user/dms.service \
|
substituteInPlace $out/lib/systemd/user/dms.service \
|
||||||
--replace-fail /usr/bin/dms $out/bin/dms \
|
--replace-fail /usr/bin/dms $out/bin/dms \
|
||||||
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
||||||
|
|
||||||
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
||||||
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
||||||
|
|
||||||
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
||||||
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
||||||
|
|
||||||
installShellCompletion --cmd dms \
|
installShellCompletion --cmd dms \
|
||||||
--bash <($out/bin/dms completion bash) \
|
--bash <($out/bin/dms completion bash) \
|
||||||
--fish <($out/bin/dms completion fish) \
|
--fish <($out/bin/dms completion fish) \
|
||||||
--zsh <($out/bin/dms completion zsh)
|
--zsh <($out/bin/dms completion zsh)
|
||||||
'';
|
'';
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
||||||
homepage = "https://danklinux.com";
|
homepage = "https://danklinux.com";
|
||||||
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
||||||
license = pkgs.lib.licenses.mit;
|
license = pkgs.lib.licenses.mit;
|
||||||
mainProgram = "dms";
|
mainProgram = "dms";
|
||||||
platforms = pkgs.lib.platforms.linux;
|
platforms = pkgs.lib.platforms.linux;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
) { };
|
||||||
|
|
||||||
quickshell = quickshell.packages.${system}.default;
|
quickshell = quickshell.packages.${system}.default;
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ qs -v -p shell.qml # Verbose debugging
|
|||||||
|
|
||||||
# Code formatting and linting
|
# Code formatting and linting
|
||||||
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format QML (don't use qmlformat)
|
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format QML (don't use qmlformat)
|
||||||
qmllint **/*.qml # Lint all QML files
|
make -C .. lint-qml # From quickshell/, call the repo-root lint target; requires the generated .qmlls.ini VFS from `qs -p .`
|
||||||
./qmlformat-all.sh # Format all QML files
|
./qmlformat-all.sh # Format all QML files
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -783,7 +783,7 @@ When modifying the shell:
|
|||||||
|
|
||||||
**QML Frontend:**
|
**QML Frontend:**
|
||||||
1. **Test changes**: `qs -p .` (automatic reload on file changes)
|
1. **Test changes**: `qs -p .` (automatic reload on file changes)
|
||||||
2. **Code quality**: Run `./qmlformat-all.sh` or `qmlformat -i **/*.qml` and `qmllint **/*.qml`
|
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 .`
|
||||||
3. **Performance**: Ensure animations remain smooth (60 FPS target)
|
3. **Performance**: Ensure animations remain smooth (60 FPS target)
|
||||||
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
|
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ Singleton {
|
|||||||
property bool _hasUnsavedChanges: false
|
property bool _hasUnsavedChanges: false
|
||||||
property var _loadedSessionSnapshot: null
|
property var _loadedSessionSnapshot: null
|
||||||
readonly property var _hooks: ({
|
readonly property var _hooks: ({
|
||||||
"updateLocale": updateLocale
|
"updateLocale": updateLocale
|
||||||
})
|
})
|
||||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||||
|
|
||||||
@@ -1245,7 +1245,7 @@ Singleton {
|
|||||||
id: greeterSessionFile
|
id: greeterSessionFile
|
||||||
|
|
||||||
path: {
|
path: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||||
return greetCfgDir + "/session.json";
|
return greetCfgDir + "/session.json";
|
||||||
}
|
}
|
||||||
preload: isGreeterMode
|
preload: isGreeterMode
|
||||||
|
|||||||
@@ -280,6 +280,7 @@ Singleton {
|
|||||||
property bool showOccupiedWorkspacesOnly: false
|
property bool showOccupiedWorkspacesOnly: false
|
||||||
property bool reverseScrolling: false
|
property bool reverseScrolling: false
|
||||||
property bool dwlShowAllTags: false
|
property bool dwlShowAllTags: false
|
||||||
|
property bool workspaceActiveAppHighlightEnabled: false
|
||||||
property string workspaceColorMode: "default"
|
property string workspaceColorMode: "default"
|
||||||
property string workspaceOccupiedColorMode: "none"
|
property string workspaceOccupiedColorMode: "none"
|
||||||
property string workspaceUnfocusedColorMode: "default"
|
property string workspaceUnfocusedColorMode: "default"
|
||||||
@@ -313,6 +314,17 @@ Singleton {
|
|||||||
property string centeringMode: "index"
|
property string centeringMode: "index"
|
||||||
property string clockDateFormat: ""
|
property string clockDateFormat: ""
|
||||||
property string lockDateFormat: ""
|
property string lockDateFormat: ""
|
||||||
|
property bool greeterRememberLastSession: true
|
||||||
|
property bool greeterRememberLastUser: true
|
||||||
|
property bool greeterEnableFprint: false
|
||||||
|
property bool greeterEnableU2f: false
|
||||||
|
property string greeterWallpaperPath: ""
|
||||||
|
property bool greeterUse24HourClock: true
|
||||||
|
property bool greeterShowSeconds: false
|
||||||
|
property bool greeterPadHours12Hour: false
|
||||||
|
property string greeterLockDateFormat: ""
|
||||||
|
property string greeterFontFamily: ""
|
||||||
|
property string greeterWallpaperFillMode: ""
|
||||||
property int mediaSize: 1
|
property int mediaSize: 1
|
||||||
|
|
||||||
property string appLauncherViewMode: "list"
|
property string appLauncherViewMode: "list"
|
||||||
@@ -456,18 +468,20 @@ Singleton {
|
|||||||
property bool matugenTemplateGhostty: true
|
property bool matugenTemplateGhostty: true
|
||||||
property bool matugenTemplateKitty: true
|
property bool matugenTemplateKitty: true
|
||||||
property bool matugenTemplateFoot: true
|
property bool matugenTemplateFoot: true
|
||||||
property bool matugenTemplateNeovim: true
|
property bool matugenTemplateNeovim: false
|
||||||
property bool matugenTemplateAlacritty: true
|
property bool matugenTemplateAlacritty: true
|
||||||
property bool matugenTemplateWezterm: true
|
property bool matugenTemplateWezterm: true
|
||||||
property bool matugenTemplateDgop: true
|
property bool matugenTemplateDgop: true
|
||||||
property bool matugenTemplateKcolorscheme: true
|
property bool matugenTemplateKcolorscheme: true
|
||||||
property bool matugenTemplateVscode: true
|
property bool matugenTemplateVscode: true
|
||||||
property bool matugenTemplateEmacs: true
|
property bool matugenTemplateEmacs: true
|
||||||
|
property bool matugenTemplateZed: true
|
||||||
|
|
||||||
property bool showDock: false
|
property bool showDock: false
|
||||||
property bool dockAutoHide: false
|
property bool dockAutoHide: false
|
||||||
property bool dockSmartAutoHide: false
|
property bool dockSmartAutoHide: false
|
||||||
property bool dockGroupByApp: false
|
property bool dockGroupByApp: false
|
||||||
|
property bool dockRestoreSpecialWorkspaceOnClick: false
|
||||||
property bool dockOpenOnOverview: false
|
property bool dockOpenOnOverview: false
|
||||||
property int dockPosition: SettingsData.Position.Bottom
|
property int dockPosition: SettingsData.Position.Bottom
|
||||||
property real dockSpacing: 4
|
property real dockSpacing: 4
|
||||||
@@ -538,6 +552,7 @@ Singleton {
|
|||||||
property bool notificationHistorySaveNormal: true
|
property bool notificationHistorySaveNormal: true
|
||||||
property bool notificationHistorySaveCritical: true
|
property bool notificationHistorySaveCritical: true
|
||||||
property var notificationRules: []
|
property var notificationRules: []
|
||||||
|
property bool notificationFocusedMonitor: false
|
||||||
|
|
||||||
property bool osdAlwaysShowValue: false
|
property bool osdAlwaysShowValue: false
|
||||||
property int osdPosition: SettingsData.Position.BottomCenter
|
property int osdPosition: SettingsData.Position.BottomCenter
|
||||||
@@ -1001,13 +1016,20 @@ Singleton {
|
|||||||
signal widgetDataChanged
|
signal widgetDataChanged
|
||||||
signal workspaceIconsUpdated
|
signal workspaceIconsUpdated
|
||||||
|
|
||||||
|
function refreshAuthAvailability() {
|
||||||
|
if (isGreeterMode)
|
||||||
|
return;
|
||||||
|
Processes.settingsRoot = root;
|
||||||
|
Processes.detectFprintd();
|
||||||
|
Processes.detectU2f();
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
Processes.settingsRoot = root;
|
Processes.settingsRoot = root;
|
||||||
loadSettings();
|
loadSettings();
|
||||||
initializeListModels();
|
initializeListModels();
|
||||||
Processes.detectFprintd();
|
refreshAuthAvailability();
|
||||||
Processes.detectU2f();
|
|
||||||
Processes.checkPluginSettings();
|
Processes.checkPluginSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1155,7 +1177,7 @@ Singleton {
|
|||||||
"updateCompositorLayout": updateCompositorLayout,
|
"updateCompositorLayout": updateCompositorLayout,
|
||||||
"applyStoredIconTheme": applyStoredIconTheme,
|
"applyStoredIconTheme": applyStoredIconTheme,
|
||||||
"updateBarConfigs": updateBarConfigs,
|
"updateBarConfigs": updateBarConfigs,
|
||||||
"updateCompositorCursor": updateCompositorCursor,
|
"updateCompositorCursor": updateCompositorCursor
|
||||||
})
|
})
|
||||||
|
|
||||||
function set(key, value) {
|
function set(key, value) {
|
||||||
@@ -1247,10 +1269,47 @@ Singleton {
|
|||||||
return JSON.stringify(Store.toJson(root), null, 2);
|
return JSON.stringify(Store.toJson(root), null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _resetPluginSettings() {
|
||||||
|
_pluginParseError = false;
|
||||||
|
pluginSettings = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function _pluginSettingsErrorCode(error) {
|
||||||
|
if (typeof error === "number")
|
||||||
|
return error;
|
||||||
|
if (error && typeof error === "object") {
|
||||||
|
if (typeof error.code === "number")
|
||||||
|
return error.code;
|
||||||
|
if (typeof error.errno === "number")
|
||||||
|
return error.errno;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = String(error || "").trim();
|
||||||
|
if (/^\d+$/.test(msg))
|
||||||
|
return Number(msg);
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isMissingPluginSettingsError(error) {
|
||||||
|
if (_pluginSettingsErrorCode(error) === 2)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const msg = String(error || "").toLowerCase();
|
||||||
|
return msg.indexOf("file does not exist") !== -1
|
||||||
|
|| msg.indexOf("no such file") !== -1
|
||||||
|
|| msg.indexOf("enoent") !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
function loadPluginSettings() {
|
function loadPluginSettings() {
|
||||||
_pluginSettingsLoading = true;
|
try {
|
||||||
parsePluginSettings(pluginSettingsFile.text());
|
parsePluginSettings(pluginSettingsFile.text());
|
||||||
_pluginSettingsLoading = false;
|
} catch (e) {
|
||||||
|
const msg = e.message || String(e);
|
||||||
|
if (!_isMissingPluginSettingsError(e))
|
||||||
|
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
|
||||||
|
_resetPluginSettings();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePluginSettings(content) {
|
function parsePluginSettings(content) {
|
||||||
@@ -2686,6 +2745,7 @@ Singleton {
|
|||||||
blockLoading: true
|
blockLoading: true
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
atomicWrites: true
|
atomicWrites: true
|
||||||
|
printErrors: false
|
||||||
watchChanges: !isGreeterMode
|
watchChanges: !isGreeterMode
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
@@ -2694,7 +2754,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
onLoadFailed: error => {
|
onLoadFailed: error => {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
pluginSettings = {};
|
const msg = String(error || "");
|
||||||
|
if (!_isMissingPluginSettingsError(error))
|
||||||
|
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
|
||||||
|
_resetPluginSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1084,7 +1084,7 @@ Singleton {
|
|||||||
|
|
||||||
property string fontFamily: {
|
property string fontFamily: {
|
||||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
||||||
return GreetdSettings.fontFamily;
|
return GreetdSettings.getEffectiveFontFamily();
|
||||||
}
|
}
|
||||||
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
||||||
}
|
}
|
||||||
@@ -1551,7 +1551,7 @@ Singleton {
|
|||||||
if (typeof SettingsData !== "undefined") {
|
if (typeof SettingsData !== "undefined") {
|
||||||
const skipTemplates = [];
|
const skipTemplates = [];
|
||||||
if (!SettingsData.runDmsMatugenTemplates) {
|
if (!SettingsData.runDmsMatugenTemplates) {
|
||||||
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs");
|
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs", "zed");
|
||||||
} else {
|
} else {
|
||||||
if (!SettingsData.matugenTemplateGtk)
|
if (!SettingsData.matugenTemplateGtk)
|
||||||
skipTemplates.push("gtk");
|
skipTemplates.push("gtk");
|
||||||
@@ -1595,6 +1595,8 @@ Singleton {
|
|||||||
skipTemplates.push("vscode");
|
skipTemplates.push("vscode");
|
||||||
if (!SettingsData.matugenTemplateEmacs)
|
if (!SettingsData.matugenTemplateEmacs)
|
||||||
skipTemplates.push("emacs");
|
skipTemplates.push("emacs");
|
||||||
|
if (!SettingsData.matugenTemplateZed)
|
||||||
|
skipTemplates.push("zed");
|
||||||
}
|
}
|
||||||
if (skipTemplates.length > 0) {
|
if (skipTemplates.length > 0) {
|
||||||
args.push("--skip-templates", skipTemplates.join(","));
|
args.push("--skip-templates", skipTemplates.join(","));
|
||||||
@@ -1987,7 +1989,7 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: dynamicColorsFileView
|
id: dynamicColorsFileView
|
||||||
path: {
|
path: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||||
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
||||||
return colorsPath;
|
return colorsPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,39 @@ Singleton {
|
|||||||
|
|
||||||
property var settingsRoot: null
|
property var settingsRoot: null
|
||||||
|
|
||||||
|
function envFlag(name) {
|
||||||
|
const value = (Quickshell.env(name) || "").trim().toLowerCase();
|
||||||
|
if (value === "1" || value === "true" || value === "yes" || value === "on")
|
||||||
|
return true;
|
||||||
|
if (value === "0" || value === "false" || value === "no" || value === "off")
|
||||||
|
return false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
|
||||||
|
readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE")
|
||||||
|
|
||||||
function detectQtTools() {
|
function detectQtTools() {
|
||||||
qtToolsDetectionProcess.running = true;
|
qtToolsDetectionProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectFprintd() {
|
function detectFprintd() {
|
||||||
|
if (!settingsRoot)
|
||||||
|
return;
|
||||||
|
if (forcedFprintAvailable !== null) {
|
||||||
|
settingsRoot.fprintdAvailable = forcedFprintAvailable;
|
||||||
|
return;
|
||||||
|
}
|
||||||
fprintdDetectionProcess.running = true;
|
fprintdDetectionProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectU2f() {
|
function detectU2f() {
|
||||||
|
if (!settingsRoot)
|
||||||
|
return;
|
||||||
|
if (forcedU2fAvailable !== null) {
|
||||||
|
settingsRoot.u2fAvailable = forcedU2fAvailable;
|
||||||
|
return;
|
||||||
|
}
|
||||||
u2fDetectionProcess.running = true;
|
u2fDetectionProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +76,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property var fprintdDetectionProcess: Process {
|
property var fprintdDetectionProcess: Process {
|
||||||
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"]
|
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1 && fprintd-list \"${USER:-$(id -un)}\" >/dev/null 2>&1"]
|
||||||
running: false
|
running: false
|
||||||
onExited: function (exitCode) {
|
onExited: function (exitCode) {
|
||||||
if (!settingsRoot)
|
if (!settingsRoot)
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ var SPEC = {
|
|||||||
showOccupiedWorkspacesOnly: { def: false },
|
showOccupiedWorkspacesOnly: { def: false },
|
||||||
reverseScrolling: { def: false },
|
reverseScrolling: { def: false },
|
||||||
dwlShowAllTags: { def: false },
|
dwlShowAllTags: { def: false },
|
||||||
|
workspaceActiveAppHighlightEnabled: { def: false },
|
||||||
workspaceColorMode: { def: "default" },
|
workspaceColorMode: { def: "default" },
|
||||||
workspaceOccupiedColorMode: { def: "none" },
|
workspaceOccupiedColorMode: { def: "none" },
|
||||||
workspaceUnfocusedColorMode: { def: "default" },
|
workspaceUnfocusedColorMode: { def: "default" },
|
||||||
@@ -164,6 +165,17 @@ var SPEC = {
|
|||||||
centeringMode: { def: "index" },
|
centeringMode: { def: "index" },
|
||||||
clockDateFormat: { def: "" },
|
clockDateFormat: { def: "" },
|
||||||
lockDateFormat: { def: "" },
|
lockDateFormat: { def: "" },
|
||||||
|
greeterRememberLastSession: { def: true },
|
||||||
|
greeterRememberLastUser: { def: true },
|
||||||
|
greeterEnableFprint: { def: false },
|
||||||
|
greeterEnableU2f: { def: false },
|
||||||
|
greeterWallpaperPath: { def: "" },
|
||||||
|
greeterUse24HourClock: { def: true },
|
||||||
|
greeterShowSeconds: { def: false },
|
||||||
|
greeterPadHours12Hour: { def: false },
|
||||||
|
greeterLockDateFormat: { def: "" },
|
||||||
|
greeterFontFamily: { def: "" },
|
||||||
|
greeterWallpaperFillMode: { def: "" },
|
||||||
mediaSize: { def: 1 },
|
mediaSize: { def: 1 },
|
||||||
|
|
||||||
appLauncherViewMode: { def: "list" },
|
appLauncherViewMode: { def: "list" },
|
||||||
@@ -272,17 +284,19 @@ var SPEC = {
|
|||||||
matugenTemplateKitty: { def: true },
|
matugenTemplateKitty: { def: true },
|
||||||
matugenTemplateFoot: { def: true },
|
matugenTemplateFoot: { def: true },
|
||||||
matugenTemplateAlacritty: { def: true },
|
matugenTemplateAlacritty: { def: true },
|
||||||
matugenTemplateNeovim: { def: true },
|
matugenTemplateNeovim: { def: false },
|
||||||
matugenTemplateWezterm: { def: true },
|
matugenTemplateWezterm: { def: true },
|
||||||
matugenTemplateDgop: { def: true },
|
matugenTemplateDgop: { def: true },
|
||||||
matugenTemplateKcolorscheme: { def: true },
|
matugenTemplateKcolorscheme: { def: true },
|
||||||
matugenTemplateVscode: { def: true },
|
matugenTemplateVscode: { def: true },
|
||||||
matugenTemplateEmacs: { def: true },
|
matugenTemplateEmacs: { def: true },
|
||||||
|
matugenTemplateZed: { def: true },
|
||||||
|
|
||||||
showDock: { def: false },
|
showDock: { def: false },
|
||||||
dockAutoHide: { def: false },
|
dockAutoHide: { def: false },
|
||||||
dockSmartAutoHide: { def: false },
|
dockSmartAutoHide: { def: false },
|
||||||
dockGroupByApp: { def: false },
|
dockGroupByApp: { def: false },
|
||||||
|
dockRestoreSpecialWorkspaceOnClick: { def: false },
|
||||||
dockOpenOnOverview: { def: false },
|
dockOpenOnOverview: { def: false },
|
||||||
dockPosition: { def: 1 },
|
dockPosition: { def: 1 },
|
||||||
dockSpacing: { def: 4 },
|
dockSpacing: { def: 4 },
|
||||||
@@ -352,6 +366,7 @@ var SPEC = {
|
|||||||
notificationHistorySaveNormal: { def: true },
|
notificationHistorySaveNormal: { def: true },
|
||||||
notificationHistorySaveCritical: { def: true },
|
notificationHistorySaveCritical: { def: true },
|
||||||
notificationRules: { def: [] },
|
notificationRules: { def: [] },
|
||||||
|
notificationFocusedMonitor: { def: false },
|
||||||
|
|
||||||
osdAlwaysShowValue: { def: false },
|
osdAlwaysShowValue: { def: false },
|
||||||
osdPosition: { def: 5 },
|
osdPosition: { def: 5 },
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Services.Greetd
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.Greetd
|
import qs.Modules.Greetd
|
||||||
|
|
||||||
Scope {
|
Scope {
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
model: SettingsData.getFilteredScreens("notifications")
|
model: SettingsData.notificationFocusedMonitor ? Quickshell.screens : SettingsData.getFilteredScreens("notifications")
|
||||||
|
|
||||||
delegate: NotificationPopupManager {
|
delegate: NotificationPopupManager {
|
||||||
modelData: item
|
modelData: item
|
||||||
|
|||||||
@@ -1006,9 +1006,7 @@ Item {
|
|||||||
_applyHighlights(newSections, searchQuery);
|
_applyHighlights(newSections, searchQuery);
|
||||||
flatModel = Scorer.flattenSections(newSections);
|
flatModel = Scorer.flattenSections(newSections);
|
||||||
sections = newSections;
|
sections = newSections;
|
||||||
if (selectedFlatIndex >= flatModel.length) {
|
selectedFlatIndex = getFirstItemIndex();
|
||||||
selectedFlatIndex = getFirstItemIndex();
|
|
||||||
}
|
|
||||||
updateSelectedItem();
|
updateSelectedItem();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ Item {
|
|||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "files":
|
case "files":
|
||||||
if (!DSearchService.dsearchAvailable)
|
if (!DSearchService.dsearchAvailable)
|
||||||
return I18n.tr("File search requires dsearch\nInstall from github.com/morelazers/dsearch");
|
return I18n.tr("File search requires dsearch\nInstall from github.com/AvengeMedia/danksearch");
|
||||||
if (!hasQuery)
|
if (!hasQuery)
|
||||||
return I18n.tr("Type to search files");
|
return I18n.tr("Type to search files");
|
||||||
if (root.controller.searchQuery.length < 2)
|
if (root.controller.searchQuery.length < 2)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -11,8 +12,45 @@ FloatingWindow {
|
|||||||
property string passwordInput: ""
|
property string passwordInput: ""
|
||||||
property var currentFlow: PolkitService.agent?.flow
|
property var currentFlow: PolkitService.agent?.flow
|
||||||
property bool isLoading: false
|
property bool isLoading: false
|
||||||
|
property bool awaitingFprintForPassword: false
|
||||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||||
|
|
||||||
|
property string polkitEtcPamText: ""
|
||||||
|
property string polkitLibPamText: ""
|
||||||
|
property string systemAuthPamText: ""
|
||||||
|
property string commonAuthPamText: ""
|
||||||
|
property string passwordAuthPamText: ""
|
||||||
|
readonly property bool polkitPamHasFprint: {
|
||||||
|
const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText;
|
||||||
|
if (!polkitText)
|
||||||
|
return false;
|
||||||
|
return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripPamComment(line) {
|
||||||
|
if (!line)
|
||||||
|
return "";
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#"))
|
||||||
|
return "";
|
||||||
|
const hashIdx = trimmed.indexOf("#");
|
||||||
|
if (hashIdx >= 0)
|
||||||
|
return trimmed.substring(0, hashIdx).trim();
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pamModuleEnabled(pamText, moduleName) {
|
||||||
|
if (!pamText || !moduleName)
|
||||||
|
return false;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (line && line.includes(moduleName))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function focusPasswordField() {
|
function focusPasswordField() {
|
||||||
passwordField.forceActiveFocus();
|
passwordField.forceActiveFocus();
|
||||||
}
|
}
|
||||||
@@ -20,6 +58,7 @@ FloatingWindow {
|
|||||||
function show() {
|
function show() {
|
||||||
passwordInput = "";
|
passwordInput = "";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
visible = true;
|
visible = true;
|
||||||
Qt.callLater(focusPasswordField);
|
Qt.callLater(focusPasswordField);
|
||||||
}
|
}
|
||||||
@@ -28,17 +67,27 @@ FloatingWindow {
|
|||||||
visible = false;
|
visible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _commitSubmit() {
|
||||||
|
isLoading = true;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
|
currentFlow.submit(passwordInput);
|
||||||
|
passwordInput = "";
|
||||||
|
}
|
||||||
|
|
||||||
function submitAuth() {
|
function submitAuth() {
|
||||||
if (!currentFlow || isLoading)
|
if (!currentFlow || isLoading)
|
||||||
return;
|
return;
|
||||||
isLoading = true;
|
if (!currentFlow.isResponseRequired) {
|
||||||
currentFlow.submit(passwordInput);
|
awaitingFprintForPassword = true;
|
||||||
passwordInput = "";
|
return;
|
||||||
|
}
|
||||||
|
_commitSubmit();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelAuth() {
|
function cancelAuth() {
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
return;
|
return;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
if (currentFlow) {
|
if (currentFlow) {
|
||||||
currentFlow.cancelAuthenticationRequest();
|
currentFlow.cancelAuthenticationRequest();
|
||||||
return;
|
return;
|
||||||
@@ -60,6 +109,7 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
passwordInput = "";
|
passwordInput = "";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -83,6 +133,11 @@ FloatingWindow {
|
|||||||
function onIsResponseRequiredChanged() {
|
function onIsResponseRequiredChanged() {
|
||||||
if (!currentFlow.isResponseRequired)
|
if (!currentFlow.isResponseRequired)
|
||||||
return;
|
return;
|
||||||
|
if (awaitingFprintForPassword && passwordInput !== "") {
|
||||||
|
_commitSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
passwordInput = "";
|
passwordInput = "";
|
||||||
passwordField.forceActiveFocus();
|
passwordField.forceActiveFocus();
|
||||||
@@ -101,6 +156,41 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/polkit-1"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.polkitEtcPamText = text()
|
||||||
|
onLoadFailed: root.polkitEtcPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/usr/lib/pam.d/polkit-1"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.polkitLibPamText = text()
|
||||||
|
onLoadFailed: root.polkitLibPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/system-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.systemAuthPamText = text()
|
||||||
|
onLoadFailed: root.systemAuthPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/common-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.commonAuthPamText = text()
|
||||||
|
onLoadFailed: root.commonAuthPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/password-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.passwordAuthPamText = text()
|
||||||
|
onLoadFailed: root.passwordAuthPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
id: contentFocusScope
|
id: contentFocusScope
|
||||||
|
|
||||||
@@ -205,36 +295,30 @@ FloatingWindow {
|
|||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
DankTextField {
|
||||||
|
id: passwordField
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: inputFieldHeight
|
height: inputFieldHeight
|
||||||
radius: Theme.cornerRadius
|
backgroundColor: Theme.surfaceHover
|
||||||
color: Theme.surfaceHover
|
normalBorderColor: Theme.outlineStrong
|
||||||
border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong
|
focusedBorderColor: Theme.primary
|
||||||
border.width: passwordField.activeFocus ? 2 : 1
|
borderWidth: 1
|
||||||
|
focusedBorderWidth: 2
|
||||||
|
leftIconName: polkitPamHasFprint ? "fingerprint" : ""
|
||||||
|
leftIconSize: 20
|
||||||
|
leftIconColor: Theme.primary
|
||||||
|
leftIconFocusedColor: Theme.primary
|
||||||
opacity: isLoading ? 0.5 : 1
|
opacity: isLoading ? 0.5 : 1
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
MouseArea {
|
textColor: Theme.surfaceText
|
||||||
anchors.fill: parent
|
text: passwordInput
|
||||||
enabled: !isLoading
|
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
|
||||||
onClicked: passwordField.forceActiveFocus()
|
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
|
||||||
}
|
placeholderText: ""
|
||||||
|
enabled: !isLoading
|
||||||
DankTextField {
|
onTextEdited: passwordInput = text
|
||||||
id: passwordField
|
onAccepted: submitAuth()
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
text: passwordInput
|
|
||||||
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
|
|
||||||
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
|
|
||||||
placeholderText: ""
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
enabled: !isLoading
|
|
||||||
onTextEdited: passwordInput = text
|
|
||||||
onAccepted: submitAuth()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
|
|||||||
@@ -241,6 +241,21 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: greeterLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 31
|
||||||
|
visible: active
|
||||||
|
focus: active
|
||||||
|
|
||||||
|
sourceComponent: GreeterTab {}
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active && item)
|
||||||
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: pluginsLoader
|
id: pluginsLoader
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -470,7 +485,7 @@ FocusScope {
|
|||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (active && item)
|
if (active && item)
|
||||||
Qt.callLater(() => item.forceActiveFocus());
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,7 +500,7 @@ FocusScope {
|
|||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (active && item)
|
if (active && item)
|
||||||
Qt.callLater(() => item.forceActiveFocus());
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,6 +287,12 @@ Rectangle {
|
|||||||
"icon": "lock",
|
"icon": "lock",
|
||||||
"tabIndex": 11
|
"tabIndex": 11
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "greeter",
|
||||||
|
"text": I18n.tr("Greeter"),
|
||||||
|
"icon": "login",
|
||||||
|
"tabIndex": 31
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "power_sleep",
|
"id": "power_sleep",
|
||||||
"text": I18n.tr("Power & Sleep"),
|
"text": I18n.tr("Power & Sleep"),
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ DankPopout {
|
|||||||
|
|
||||||
layerNamespace: "dms:app-launcher"
|
layerNamespace: "dms:app-launcher"
|
||||||
|
|
||||||
|
readonly property real screenWidth: screen?.width ?? 1920
|
||||||
|
readonly property real screenHeight: screen?.height ?? 1080
|
||||||
|
|
||||||
property string _pendingMode: ""
|
property string _pendingMode: ""
|
||||||
property string _pendingQuery: ""
|
property string _pendingQuery: ""
|
||||||
|
|
||||||
@@ -41,8 +44,35 @@ DankPopout {
|
|||||||
openWithQuery(query);
|
openWithQuery(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
popupWidth: 560
|
readonly property int _baseWidth: {
|
||||||
popupHeight: 640
|
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)
|
||||||
|
|
||||||
triggerWidth: 40
|
triggerWidth: 40
|
||||||
positioning: ""
|
positioning: ""
|
||||||
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
|
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ DankPopout {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
layerNamespace: "dms:control-center"
|
layerNamespace: "dms:control-center"
|
||||||
fullHeightSurface: false
|
fullHeightSurface: true
|
||||||
|
|
||||||
property string expandedSection: ""
|
property string expandedSection: ""
|
||||||
property var triggerScreen: null
|
property var triggerScreen: null
|
||||||
|
|||||||
@@ -390,10 +390,11 @@ BasePill {
|
|||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
NumericText {
|
||||||
id: audioPercentV
|
id: audioPercentV
|
||||||
visible: root.showAudioPercent
|
visible: root.showAudioPercent
|
||||||
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
||||||
|
reserveText: "100%"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
@@ -416,10 +417,11 @@ BasePill {
|
|||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
NumericText {
|
||||||
id: micPercentV
|
id: micPercentV
|
||||||
visible: root.showMicPercent
|
visible: root.showMicPercent
|
||||||
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
||||||
|
reserveText: "100%"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
@@ -442,10 +444,11 @@ BasePill {
|
|||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
NumericText {
|
||||||
id: brightnessPercentV
|
id: brightnessPercentV
|
||||||
visible: root.showBrightnessPercent
|
visible: root.showBrightnessPercent
|
||||||
text: Math.round(getBrightness() * 100) + "%"
|
text: Math.round(getBrightness() * 100) + "%"
|
||||||
|
reserveText: "100%"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
@@ -536,7 +539,8 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: audioIcon.implicitWidth + (root.showAudioPercent ? audioPercent.implicitWidth : 0) + 4
|
width: audioIcon.implicitWidth + (root.showAudioPercent ? audioPercent.reservedWidth : 0) + 4
|
||||||
|
implicitWidth: width
|
||||||
height: root.widgetThickness - root.horizontalPadding * 2
|
height: root.widgetThickness - root.horizontalPadding * 2
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -552,20 +556,23 @@ BasePill {
|
|||||||
anchors.leftMargin: 2
|
anchors.leftMargin: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
NumericText {
|
||||||
id: audioPercent
|
id: audioPercent
|
||||||
visible: root.showAudioPercent
|
visible: root.showAudioPercent
|
||||||
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
||||||
|
reserveText: "100%"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: audioIcon.right
|
anchors.left: audioIcon.right
|
||||||
anchors.leftMargin: 2
|
anchors.leftMargin: 2
|
||||||
|
width: reservedWidth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: micIcon.implicitWidth + (root.showMicPercent ? micPercent.implicitWidth : 0) + 4
|
width: micIcon.implicitWidth + (root.showMicPercent ? micPercent.reservedWidth : 0) + 4
|
||||||
|
implicitWidth: width
|
||||||
height: root.widgetThickness - root.horizontalPadding * 2
|
height: root.widgetThickness - root.horizontalPadding * 2
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -581,20 +588,22 @@ BasePill {
|
|||||||
anchors.leftMargin: 2
|
anchors.leftMargin: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
NumericText {
|
||||||
id: micPercent
|
id: micPercent
|
||||||
visible: root.showMicPercent
|
visible: root.showMicPercent
|
||||||
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
||||||
|
reserveText: "100%"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: micIcon.right
|
anchors.left: micIcon.right
|
||||||
anchors.leftMargin: 2
|
anchors.leftMargin: 2
|
||||||
|
width: reservedWidth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: brightnessIcon.implicitWidth + (root.showBrightnessPercent ? brightnessPercent.implicitWidth : 0) + 4
|
width: brightnessIcon.implicitWidth + (root.showBrightnessPercent ? brightnessPercent.reservedWidth : 0) + 4
|
||||||
height: root.widgetThickness - root.horizontalPadding * 2
|
height: root.widgetThickness - root.horizontalPadding * 2
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -610,15 +619,17 @@ BasePill {
|
|||||||
anchors.leftMargin: 2
|
anchors.leftMargin: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
NumericText {
|
||||||
id: brightnessPercent
|
id: brightnessPercent
|
||||||
visible: root.showBrightnessPercent
|
visible: root.showBrightnessPercent
|
||||||
text: Math.round(getBrightness() * 100) + "%"
|
text: Math.round(getBrightness() * 100) + "%"
|
||||||
|
reserveText: "100%"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: brightnessIcon.right
|
anchors.left: brightnessIcon.right
|
||||||
anchors.leftMargin: 2
|
anchors.leftMargin: 2
|
||||||
|
width: reservedWidth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -211,16 +211,17 @@ BasePill {
|
|||||||
text: {
|
text: {
|
||||||
const title = activeWindow && activeWindow.title ? activeWindow.title : "";
|
const title = activeWindow && activeWindow.title ? activeWindow.title : "";
|
||||||
const appName = appText.text;
|
const appName = appText.text;
|
||||||
|
|
||||||
|
if (compactMode && title === appName) {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
if (!title || !appName) {
|
if (!title || !appName) {
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (title.endsWith(" - " + appName)) {
|
|
||||||
return title.substring(0, title.length - (" - " + appName).length);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (title.endsWith(appName)) {
|
if (title.endsWith(appName)) {
|
||||||
return title.substring(0, title.length - appName.length).replace(/ - $/, "");
|
return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
return title;
|
return title;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
import Quickshell.Services.SystemTray
|
import Quickshell.Services.SystemTray
|
||||||
@@ -162,6 +161,23 @@ BasePill {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property string autoBarShadowDirection: {
|
||||||
|
const edge = root.axis?.edge;
|
||||||
|
switch (edge) {
|
||||||
|
case "top":
|
||||||
|
return "top";
|
||||||
|
case "bottom":
|
||||||
|
return "bottom";
|
||||||
|
case "left":
|
||||||
|
return "left";
|
||||||
|
case "right":
|
||||||
|
return "right";
|
||||||
|
default:
|
||||||
|
return "bottom";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
|
||||||
|
|
||||||
property bool menuOpen: false
|
property bool menuOpen: false
|
||||||
property var currentTrayMenu: null
|
property var currentTrayMenu: null
|
||||||
|
|
||||||
@@ -940,13 +956,6 @@ BasePill {
|
|||||||
}
|
}
|
||||||
})(), overflowMenu.dpr)
|
})(), overflowMenu.dpr)
|
||||||
|
|
||||||
readonly property var elev: Theme.elevationLevel2
|
|
||||||
property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8
|
|
||||||
property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0
|
|
||||||
property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25
|
|
||||||
readonly property real popupSurfaceAlpha: Theme.popupTransparency
|
|
||||||
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
|
|
||||||
|
|
||||||
opacity: root.menuOpen ? 1 : 0
|
opacity: root.menuOpen ? 1 : 0
|
||||||
scale: root.menuOpen ? 1 : 0.85
|
scale: root.menuOpen ? 1 : 0.85
|
||||||
|
|
||||||
@@ -967,19 +976,14 @@ BasePill {
|
|||||||
ElevationShadow {
|
ElevationShadow {
|
||||||
id: bgShadowLayer
|
id: bgShadowLayer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
level: menuContainer.elev
|
level: Theme.elevationLevel3
|
||||||
fallbackOffset: 4
|
direction: root.effectiveShadowDirection
|
||||||
shadowBlurPx: menuContainer.shadowBlurPx
|
fallbackOffset: 6
|
||||||
shadowSpreadPx: menuContainer.shadowSpreadPx
|
|
||||||
shadowColor: {
|
|
||||||
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
|
|
||||||
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
|
|
||||||
}
|
|
||||||
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
targetRadius: Theme.cornerRadius
|
targetRadius: Theme.cornerRadius
|
||||||
sourceRect.antialiasing: true
|
sourceRect.antialiasing: true
|
||||||
sourceRect.smooth: true
|
sourceRect.smooth: true
|
||||||
shadowEnabled: Theme.elevationEnabled
|
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
|
||||||
layer.smooth: true
|
layer.smooth: true
|
||||||
layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
|
layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
|
||||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||||
@@ -1402,13 +1406,6 @@ BasePill {
|
|||||||
}
|
}
|
||||||
})(), menuWindow.dpr)
|
})(), menuWindow.dpr)
|
||||||
|
|
||||||
readonly property var elev: Theme.elevationLevel2
|
|
||||||
property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8
|
|
||||||
property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0
|
|
||||||
property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25
|
|
||||||
readonly property real popupSurfaceAlpha: Theme.popupTransparency
|
|
||||||
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
|
|
||||||
|
|
||||||
opacity: menuRoot.showMenu ? 1 : 0
|
opacity: menuRoot.showMenu ? 1 : 0
|
||||||
scale: menuRoot.showMenu ? 1 : 0.85
|
scale: menuRoot.showMenu ? 1 : 0.85
|
||||||
|
|
||||||
@@ -1429,18 +1426,13 @@ BasePill {
|
|||||||
ElevationShadow {
|
ElevationShadow {
|
||||||
id: menuBgShadowLayer
|
id: menuBgShadowLayer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
level: menuContainer.elev
|
level: Theme.elevationLevel3
|
||||||
fallbackOffset: 4
|
direction: root.effectiveShadowDirection
|
||||||
shadowBlurPx: menuContainer.shadowBlurPx
|
fallbackOffset: 6
|
||||||
shadowSpreadPx: menuContainer.shadowSpreadPx
|
|
||||||
shadowColor: {
|
|
||||||
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
|
|
||||||
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
|
|
||||||
}
|
|
||||||
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
targetRadius: Theme.cornerRadius
|
targetRadius: Theme.cornerRadius
|
||||||
sourceRect.antialiasing: true
|
sourceRect.antialiasing: true
|
||||||
shadowEnabled: Theme.elevationEnabled
|
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
|
||||||
layer.smooth: true
|
layer.smooth: true
|
||||||
layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
|
layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
|
||||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||||
|
|||||||
@@ -1470,6 +1470,10 @@ Item {
|
|||||||
delegate: Item {
|
delegate: Item {
|
||||||
width: root.appIconSize
|
width: root.appIconSize
|
||||||
height: root.appIconSize
|
height: root.appIconSize
|
||||||
|
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
|
||||||
|
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
|
||||||
|
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
|
||||||
|
readonly property real appOpacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
id: rowAppIcon
|
id: rowAppIcon
|
||||||
@@ -1485,14 +1489,14 @@ Item {
|
|||||||
color: Theme.surfaceContainer
|
color: Theme.surfaceContainer
|
||||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: Theme.primarySelected
|
border.color: appBorderColor
|
||||||
opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: appOpacity
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
|
text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
|
||||||
font.pixelSize: parent.width * 0.45
|
font.pixelSize: parent.width * 0.45
|
||||||
color: Theme.primary
|
color: appGlyphColor
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1503,14 +1507,14 @@ Item {
|
|||||||
color: Theme.surfaceContainer
|
color: Theme.surfaceContainer
|
||||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: Theme.primarySelected
|
border.color: appBorderColor
|
||||||
opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: appOpacity
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
size: parent.width * 0.7
|
size: parent.width * 0.7
|
||||||
name: "sports_esports"
|
name: "sports_esports"
|
||||||
color: Theme.primary
|
color: appGlyphColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1523,11 +1527,12 @@ Item {
|
|||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
saturation: 0
|
saturation: 0
|
||||||
colorization: 1
|
colorization: 1
|
||||||
colorizationColor: isActive ? quickshellIconActiveColor : quickshellIconInactiveColor
|
colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
|
id: rowSteamIcon
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: modelData.icon
|
source: modelData.icon
|
||||||
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||||
@@ -1538,11 +1543,21 @@ Item {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
size: root.appIconSize
|
size: root.appIconSize
|
||||||
name: "sports_esports"
|
name: "sports_esports"
|
||||||
color: Theme.widgetTextColor
|
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
|
||||||
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||||
visible: modelData.isSteamApp && !modelData.icon
|
visible: modelData.isSteamApp && !modelData.icon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: (rowAppIcon.visible || rowSteamIcon.visible || modelData.isQuickshell) && appHighlightActive
|
||||||
|
color: "transparent"
|
||||||
|
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||||
|
border.width: 1
|
||||||
|
border.color: focusedBorderColor
|
||||||
|
z: 1
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: rowAppMouseArea
|
id: rowAppMouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -1624,6 +1639,10 @@ Item {
|
|||||||
delegate: Item {
|
delegate: Item {
|
||||||
width: root.appIconSize
|
width: root.appIconSize
|
||||||
height: root.appIconSize
|
height: root.appIconSize
|
||||||
|
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
|
||||||
|
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
|
||||||
|
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
|
||||||
|
readonly property real appOpacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
id: colAppIcon
|
id: colAppIcon
|
||||||
@@ -1639,14 +1658,14 @@ Item {
|
|||||||
color: Theme.surfaceContainer
|
color: Theme.surfaceContainer
|
||||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: Theme.primarySelected
|
border.color: appBorderColor
|
||||||
opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: appOpacity
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
|
text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
|
||||||
font.pixelSize: parent.width * 0.45
|
font.pixelSize: parent.width * 0.45
|
||||||
color: Theme.primary
|
color: appGlyphColor
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1657,14 +1676,14 @@ Item {
|
|||||||
color: Theme.surfaceContainer
|
color: Theme.surfaceContainer
|
||||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: Theme.primarySelected
|
border.color: appBorderColor
|
||||||
opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: appOpacity
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
size: parent.width * 0.7
|
size: parent.width * 0.7
|
||||||
name: "sports_esports"
|
name: "sports_esports"
|
||||||
color: Theme.primary
|
color: appGlyphColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1677,11 +1696,12 @@ Item {
|
|||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
saturation: 0
|
saturation: 0
|
||||||
colorization: 1
|
colorization: 1
|
||||||
colorizationColor: isActive ? quickshellIconActiveColor : quickshellIconInactiveColor
|
colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
|
id: colSteamIcon
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: modelData.icon
|
source: modelData.icon
|
||||||
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||||
@@ -1692,11 +1712,21 @@ Item {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
size: root.appIconSize
|
size: root.appIconSize
|
||||||
name: "sports_esports"
|
name: "sports_esports"
|
||||||
color: Theme.widgetTextColor
|
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
|
||||||
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||||
visible: modelData.isSteamApp && !modelData.icon
|
visible: modelData.isSteamApp && !modelData.icon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: (colAppIcon.visible || colSteamIcon.visible || modelData.isQuickshell) && appHighlightActive
|
||||||
|
color: "transparent"
|
||||||
|
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||||
|
border.width: 1
|
||||||
|
border.color: focusedBorderColor
|
||||||
|
z: 1
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: colAppMouseArea
|
id: colAppMouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
@@ -133,6 +134,40 @@ Item {
|
|||||||
function getGroupedToplevels() {
|
function getGroupedToplevels() {
|
||||||
return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || [];
|
return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getHyprToplevelForWayland(waylandToplevel) {
|
||||||
|
if (!waylandToplevel || !CompositorService.isHyprland || !Hyprland.toplevels)
|
||||||
|
return null;
|
||||||
|
const hyprToplevels = Array.from(Hyprland.toplevels.values);
|
||||||
|
for (let i = 0; i < hyprToplevels.length; i++) {
|
||||||
|
if (hyprToplevels[i].wayland === waylandToplevel)
|
||||||
|
return hyprToplevels[i];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSpecialWorkspaceName(waylandToplevel) {
|
||||||
|
const hyprToplevel = getHyprToplevelForWayland(waylandToplevel);
|
||||||
|
if (!hyprToplevel)
|
||||||
|
return "";
|
||||||
|
const wsName = String(hyprToplevel.lastIpcObject?.workspace?.name || hyprToplevel.workspace?.name || "");
|
||||||
|
if (!wsName.startsWith("special:"))
|
||||||
|
return "";
|
||||||
|
return wsName.slice("special:".length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreSpecialWorkspaceWindow(waylandToplevel) {
|
||||||
|
if (!SettingsData.dockRestoreSpecialWorkspaceOnClick || !CompositorService.isHyprland || !waylandToplevel)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const specialName = getSpecialWorkspaceName(waylandToplevel);
|
||||||
|
if (!specialName)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Hyprland.dispatch("togglespecialworkspace " + specialName);
|
||||||
|
Qt.callLater(() => waylandToplevel.activate());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
onIsHoveredChanged: {
|
onIsHoveredChanged: {
|
||||||
if (mouseArea.pressed || dragging)
|
if (mouseArea.pressed || dragging)
|
||||||
return;
|
return;
|
||||||
@@ -276,8 +311,11 @@ Item {
|
|||||||
break;
|
break;
|
||||||
case "window":
|
case "window":
|
||||||
const windowToplevel = getToplevelObject();
|
const windowToplevel = getToplevelObject();
|
||||||
if (windowToplevel)
|
if (windowToplevel) {
|
||||||
|
if (restoreSpecialWorkspaceWindow(windowToplevel))
|
||||||
|
return;
|
||||||
windowToplevel.activate();
|
windowToplevel.activate();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "grouped":
|
case "grouped":
|
||||||
if (appData.windowCount === 0) {
|
if (appData.windowCount === 0) {
|
||||||
@@ -300,8 +338,11 @@ Item {
|
|||||||
SessionService.launchDesktopEntry(groupedEntry);
|
SessionService.launchDesktopEntry(groupedEntry);
|
||||||
} else if (appData.windowCount === 1) {
|
} else if (appData.windowCount === 1) {
|
||||||
const groupedToplevel = getToplevelObject();
|
const groupedToplevel = getToplevelObject();
|
||||||
if (groupedToplevel)
|
if (groupedToplevel) {
|
||||||
|
if (restoreSpecialWorkspaceWindow(groupedToplevel))
|
||||||
|
return;
|
||||||
groupedToplevel.activate();
|
groupedToplevel.activate();
|
||||||
|
}
|
||||||
} else if (contextMenu) {
|
} else if (contextMenu) {
|
||||||
const shouldHidePin = appData.appId === "org.quickshell";
|
const shouldHidePin = appData.appId === "org.quickshell";
|
||||||
contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps);
|
contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps);
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
.pragma library
|
||||||
|
|
||||||
|
function readBoolOverride(envReader, names, fallbackValue) {
|
||||||
|
for (let i = 0; i < names.length; i++) {
|
||||||
|
const name = names[i];
|
||||||
|
const raw = envReader(name);
|
||||||
|
if (raw === undefined || raw === null || raw === "")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const normalized = String(raw).trim().toLowerCase();
|
||||||
|
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on")
|
||||||
|
return true;
|
||||||
|
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
console.warn("Invalid boolean override for", name + ":", raw, "- trying next override/fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackValue;
|
||||||
|
}
|
||||||
@@ -4,13 +4,16 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import "GreetdEnv.js" as GreetdEnv
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
|
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
||||||
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
|
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
|
||||||
readonly property string memoryFile: greetCfgDir + "/memory.json"
|
readonly property string memoryFile: greetCfgDir + "/.local/state/memory.json"
|
||||||
|
readonly property bool rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true)
|
||||||
|
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
|
||||||
|
|
||||||
property string lastSessionId: ""
|
property string lastSessionId: ""
|
||||||
property string lastSuccessfulUser: ""
|
property string lastSuccessfulUser: ""
|
||||||
@@ -49,26 +52,44 @@ Singleton {
|
|||||||
if (!content || !content.trim())
|
if (!content || !content.trim())
|
||||||
return;
|
return;
|
||||||
const memory = JSON.parse(content);
|
const memory = JSON.parse(content);
|
||||||
lastSessionId = memory.lastSessionId || "";
|
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : "";
|
||||||
lastSuccessfulUser = memory.lastSuccessfulUser || "";
|
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
|
||||||
|
if (!rememberLastSession || !rememberLastUser)
|
||||||
|
saveMemory();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to parse greetd memory:", e);
|
console.warn("Failed to parse greetd memory:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveMemory() {
|
function saveMemory() {
|
||||||
memoryFileView.setText(JSON.stringify({
|
let memory = {};
|
||||||
"lastSessionId": lastSessionId,
|
if (rememberLastSession && lastSessionId)
|
||||||
"lastSuccessfulUser": lastSuccessfulUser
|
memory.lastSessionId = lastSessionId;
|
||||||
}, null, 2));
|
if (rememberLastUser && lastSuccessfulUser)
|
||||||
|
memory.lastSuccessfulUser = lastSuccessfulUser;
|
||||||
|
memoryFileView.setText(JSON.stringify(memory, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLastSessionId(id) {
|
function setLastSessionId(id) {
|
||||||
|
if (!rememberLastSession) {
|
||||||
|
if (lastSessionId !== "") {
|
||||||
|
lastSessionId = "";
|
||||||
|
saveMemory();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
lastSessionId = id || "";
|
lastSessionId = id || "";
|
||||||
saveMemory();
|
saveMemory();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLastSuccessfulUser(username) {
|
function setLastSuccessfulUser(username) {
|
||||||
|
if (!rememberLastUser) {
|
||||||
|
if (lastSuccessfulUser !== "") {
|
||||||
|
lastSuccessfulUser = "";
|
||||||
|
saveMemory();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
lastSuccessfulUser = username || "";
|
lastSuccessfulUser = username || "";
|
||||||
saveMemory();
|
saveMemory();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,22 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import "GreetdEnv.js" as GreetdEnv
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property string configPath: {
|
readonly property string configPath: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||||
return greetCfgDir + "/settings.json";
|
return greetCfgDir + "/settings.json";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property string _greeterCacheDir: {
|
||||||
|
const i = root.configPath.lastIndexOf("/");
|
||||||
|
return i >= 0 ? root.configPath.substring(0, i) : "";
|
||||||
|
}
|
||||||
|
readonly property string greeterWallpaperOverridePath: root._greeterCacheDir ? (root._greeterCacheDir + "/greeter_wallpaper_override.jpg") : ""
|
||||||
|
|
||||||
property string currentThemeName: "purple"
|
property string currentThemeName: "purple"
|
||||||
property bool settingsLoaded: false
|
property bool settingsLoaded: false
|
||||||
property string customThemeFile: ""
|
property string customThemeFile: ""
|
||||||
@@ -21,6 +28,12 @@ Singleton {
|
|||||||
property bool use24HourClock: true
|
property bool use24HourClock: true
|
||||||
property bool showSeconds: false
|
property bool showSeconds: false
|
||||||
property bool padHours12Hour: false
|
property bool padHours12Hour: false
|
||||||
|
property bool greeterUse24HourClock: true
|
||||||
|
property bool greeterShowSeconds: false
|
||||||
|
property bool greeterPadHours12Hour: false
|
||||||
|
property string greeterLockDateFormat: ""
|
||||||
|
property string greeterFontFamily: ""
|
||||||
|
property string greeterWallpaperFillMode: ""
|
||||||
property bool useFahrenheit: false
|
property bool useFahrenheit: false
|
||||||
property bool nightModeEnabled: false
|
property bool nightModeEnabled: false
|
||||||
property string weatherLocation: "New York, NY"
|
property string weatherLocation: "New York, NY"
|
||||||
@@ -41,6 +54,11 @@ Singleton {
|
|||||||
property string lockDateFormat: ""
|
property string lockDateFormat: ""
|
||||||
property bool lockScreenShowPowerActions: true
|
property bool lockScreenShowPowerActions: true
|
||||||
property bool lockScreenShowProfileImage: true
|
property bool lockScreenShowProfileImage: true
|
||||||
|
property bool rememberLastSession: true
|
||||||
|
property bool rememberLastUser: true
|
||||||
|
property bool greeterEnableFprint: false
|
||||||
|
property bool greeterEnableU2f: false
|
||||||
|
property string greeterWallpaperPath: ""
|
||||||
property bool powerActionConfirm: true
|
property bool powerActionConfirm: true
|
||||||
property real powerActionHoldDuration: 0.5
|
property real powerActionHoldDuration: 0.5
|
||||||
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
||||||
@@ -52,66 +70,103 @@ Singleton {
|
|||||||
|
|
||||||
function parseSettings(content) {
|
function parseSettings(content) {
|
||||||
try {
|
try {
|
||||||
|
let settings = {};
|
||||||
if (content && content.trim()) {
|
if (content && content.trim()) {
|
||||||
const settings = JSON.parse(content);
|
settings = JSON.parse(content);
|
||||||
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
|
}
|
||||||
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
|
|
||||||
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
|
|
||||||
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
|
|
||||||
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
|
|
||||||
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
|
|
||||||
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
|
|
||||||
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
|
|
||||||
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
|
|
||||||
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
|
|
||||||
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false;
|
|
||||||
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true;
|
|
||||||
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default";
|
|
||||||
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false;
|
|
||||||
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "";
|
|
||||||
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5;
|
|
||||||
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1;
|
|
||||||
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : Theme.defaultFontFamily;
|
|
||||||
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : Theme.defaultMonoFontFamily;
|
|
||||||
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
|
|
||||||
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0;
|
|
||||||
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12;
|
|
||||||
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
|
|
||||||
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
|
|
||||||
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
|
|
||||||
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
|
|
||||||
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
|
|
||||||
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
|
|
||||||
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
|
|
||||||
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
|
|
||||||
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
|
|
||||||
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
|
|
||||||
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
|
|
||||||
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
|
|
||||||
settingsLoaded = true;
|
|
||||||
|
|
||||||
if (typeof Theme !== "undefined") {
|
const envRememberLastSession = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], undefined);
|
||||||
if (currentThemeName === "custom" && customThemeFile) {
|
const envRememberLastUser = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], undefined);
|
||||||
Theme.loadCustomThemeFromFile(customThemeFile);
|
|
||||||
}
|
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
|
||||||
Theme.applyGreeterTheme(currentThemeName);
|
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
|
||||||
|
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
|
||||||
|
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
|
||||||
|
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
|
||||||
|
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
|
||||||
|
greeterUse24HourClock = settings.greeterUse24HourClock !== undefined ? settings.greeterUse24HourClock : use24HourClock;
|
||||||
|
greeterShowSeconds = settings.greeterShowSeconds !== undefined ? settings.greeterShowSeconds : showSeconds;
|
||||||
|
greeterPadHours12Hour = settings.greeterPadHours12Hour !== undefined ? settings.greeterPadHours12Hour : padHours12Hour;
|
||||||
|
greeterLockDateFormat = settings.greeterLockDateFormat !== undefined ? settings.greeterLockDateFormat : "";
|
||||||
|
greeterFontFamily = settings.greeterFontFamily !== undefined ? settings.greeterFontFamily : "";
|
||||||
|
greeterWallpaperFillMode = settings.greeterWallpaperFillMode !== undefined ? settings.greeterWallpaperFillMode : "";
|
||||||
|
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
|
||||||
|
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
|
||||||
|
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
|
||||||
|
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
|
||||||
|
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false;
|
||||||
|
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true;
|
||||||
|
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default";
|
||||||
|
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false;
|
||||||
|
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "";
|
||||||
|
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5;
|
||||||
|
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1;
|
||||||
|
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : Theme.defaultFontFamily;
|
||||||
|
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : Theme.defaultMonoFontFamily;
|
||||||
|
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
|
||||||
|
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0;
|
||||||
|
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12;
|
||||||
|
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
|
||||||
|
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
|
||||||
|
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
|
||||||
|
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
|
||||||
|
if (envRememberLastSession !== undefined) {
|
||||||
|
rememberLastSession = envRememberLastSession;
|
||||||
|
} else {
|
||||||
|
rememberLastSession = settings.greeterRememberLastSession !== undefined ? settings.greeterRememberLastSession : settings.rememberLastSession !== undefined ? settings.rememberLastSession : true;
|
||||||
|
}
|
||||||
|
if (envRememberLastUser !== undefined) {
|
||||||
|
rememberLastUser = envRememberLastUser;
|
||||||
|
} else {
|
||||||
|
rememberLastUser = settings.greeterRememberLastUser !== undefined ? settings.greeterRememberLastUser : settings.rememberLastUser !== undefined ? settings.rememberLastUser : true;
|
||||||
|
}
|
||||||
|
greeterEnableFprint = settings.greeterEnableFprint !== undefined ? settings.greeterEnableFprint : false;
|
||||||
|
greeterEnableU2f = settings.greeterEnableU2f !== undefined ? settings.greeterEnableU2f : false;
|
||||||
|
greeterWallpaperPath = settings.greeterWallpaperPath !== undefined ? settings.greeterWallpaperPath : "";
|
||||||
|
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
|
||||||
|
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
|
||||||
|
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
|
||||||
|
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
|
||||||
|
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
|
||||||
|
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
|
||||||
|
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
|
||||||
|
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
|
||||||
|
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
if (currentThemeName === "custom" && customThemeFile) {
|
||||||
|
Theme.loadCustomThemeFromFile(customThemeFile);
|
||||||
}
|
}
|
||||||
|
Theme.applyGreeterTheme(currentThemeName);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to parse greetd settings:", e);
|
console.warn("Failed to parse greetd settings:", e);
|
||||||
|
} finally {
|
||||||
|
settingsLoaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEffectiveTimeFormat() {
|
function getEffectiveTimeFormat() {
|
||||||
if (use24HourClock)
|
const use24 = greeterUse24HourClock;
|
||||||
return showSeconds ? "hh:mm:ss" : "hh:mm";
|
const secs = greeterShowSeconds;
|
||||||
if (padHours12Hour)
|
const pad = greeterPadHours12Hour;
|
||||||
return showSeconds ? "hh:mm:ss AP" : "hh:mm AP";
|
if (use24)
|
||||||
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
|
return secs ? "hh:mm:ss" : "hh:mm";
|
||||||
|
if (pad)
|
||||||
|
return secs ? "hh:mm:ss AP" : "hh:mm AP";
|
||||||
|
return secs ? "h:mm:ss AP" : "h:mm AP";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEffectiveLockDateFormat() {
|
function getEffectiveLockDateFormat() {
|
||||||
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat;
|
const fmt = (greeterLockDateFormat !== undefined && greeterLockDateFormat !== "") ? greeterLockDateFormat : lockDateFormat;
|
||||||
|
return fmt && fmt.length > 0 ? fmt : Locale.LongFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveWallpaperFillMode() {
|
||||||
|
return (greeterWallpaperFillMode && greeterWallpaperFillMode !== "") ? greeterWallpaperFillMode : wallpaperFillMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveFontFamily() {
|
||||||
|
return (greeterFontFamily && greeterFontFamily !== "") ? greeterFontFamily : fontFamily;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilteredScreens(componentId) {
|
function getFilteredScreens(componentId) {
|
||||||
@@ -133,5 +188,9 @@ Singleton {
|
|||||||
onLoaded: {
|
onLoaded: {
|
||||||
parseSettings(settingsFile.text());
|
parseSettings(settingsFile.text());
|
||||||
}
|
}
|
||||||
|
onLoadFailed: error => {
|
||||||
|
console.warn("Failed to load greetd settings:", error);
|
||||||
|
root.parseSettings("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,31 @@ Item {
|
|||||||
signal launchRequested
|
signal launchRequested
|
||||||
|
|
||||||
property bool weatherInitialized: false
|
property bool weatherInitialized: false
|
||||||
|
property bool awaitingExternalAuth: false
|
||||||
|
property bool pendingPasswordResponse: false
|
||||||
|
property bool passwordSubmitRequested: false
|
||||||
|
property bool cancelingExternalAuthForPassword: false
|
||||||
|
property int defaultAuthTimeoutMs: 10000
|
||||||
|
property int externalAuthTimeoutMs: 36000
|
||||||
|
property int memoryFlushDelayMs: 120
|
||||||
|
property string pendingLaunchCommand: ""
|
||||||
|
property var pendingLaunchEnv: []
|
||||||
|
property int passwordFailureCount: 0
|
||||||
|
property int passwordAttemptLimitHint: 0
|
||||||
|
property string authFeedbackMessage: ""
|
||||||
|
property string greetdPamText: ""
|
||||||
|
property string systemAuthPamText: ""
|
||||||
|
property string commonAuthPamText: ""
|
||||||
|
property string passwordAuthPamText: ""
|
||||||
|
property string faillockConfigText: ""
|
||||||
|
property bool greeterWallpaperOverrideExists: false
|
||||||
|
property string externalAuthAutoStartedForUser: ""
|
||||||
|
property int passwordSessionTransitionRetryCount: 0
|
||||||
|
property int maxPasswordSessionTransitionRetries: 2
|
||||||
|
readonly property bool greeterPamHasFprint: pamModuleEnabled(greetdPamText, "pam_fprintd") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"))
|
||||||
|
readonly property bool greeterPamHasU2f: pamModuleEnabled(greetdPamText, "pam_u2f") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_u2f")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_u2f")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_u2f"))
|
||||||
|
readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f)
|
||||||
|
readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f
|
||||||
|
|
||||||
function initWeatherService() {
|
function initWeatherService() {
|
||||||
if (weatherInitialized)
|
if (weatherInitialized)
|
||||||
@@ -44,16 +69,260 @@ Item {
|
|||||||
WeatherService.forceRefresh();
|
WeatherService.forceRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripPamComment(line) {
|
||||||
|
if (!line)
|
||||||
|
return "";
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#"))
|
||||||
|
return "";
|
||||||
|
const hashIdx = trimmed.indexOf("#");
|
||||||
|
if (hashIdx >= 0)
|
||||||
|
return trimmed.substring(0, hashIdx).trim();
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pamModuleEnabled(pamText, moduleName) {
|
||||||
|
if (!pamText || !moduleName)
|
||||||
|
return false;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
if (line.includes(moduleName))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usesPamLockoutPolicy(pamText) {
|
||||||
|
if (!pamText)
|
||||||
|
return false;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
if (line.includes("pam_faillock.so") || line.includes("pam_tally2.so") || line.includes("pam_tally.so"))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePamLineDenyValue(pamText) {
|
||||||
|
if (!pamText)
|
||||||
|
return -1;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
if (!line.includes("pam_faillock.so") && !line.includes("pam_tally2.so") && !line.includes("pam_tally.so"))
|
||||||
|
continue;
|
||||||
|
const denyMatch = line.match(/\bdeny\s*=\s*(\d+)\b/i);
|
||||||
|
if (!denyMatch)
|
||||||
|
continue;
|
||||||
|
const parsed = parseInt(denyMatch[1], 10);
|
||||||
|
if (!isNaN(parsed))
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFaillockDenyValue(configText) {
|
||||||
|
if (!configText)
|
||||||
|
return -1;
|
||||||
|
const lines = configText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
const denyMatch = line.match(/^deny\s*=\s*(\d+)\s*$/i);
|
||||||
|
if (!denyMatch)
|
||||||
|
continue;
|
||||||
|
const parsed = parseInt(denyMatch[1], 10);
|
||||||
|
if (!isNaN(parsed))
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshPasswordAttemptPolicyHint() {
|
||||||
|
const pamSources = [greetdPamText, systemAuthPamText, commonAuthPamText, passwordAuthPamText];
|
||||||
|
let lockoutConfigured = false;
|
||||||
|
let denyFromPam = -1;
|
||||||
|
for (let i = 0; i < pamSources.length; i++) {
|
||||||
|
const source = pamSources[i];
|
||||||
|
if (!source)
|
||||||
|
continue;
|
||||||
|
if (usesPamLockoutPolicy(source))
|
||||||
|
lockoutConfigured = true;
|
||||||
|
const denyValue = parsePamLineDenyValue(source);
|
||||||
|
if (denyValue >= 0 && (denyFromPam < 0 || denyValue < denyFromPam))
|
||||||
|
denyFromPam = denyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lockoutConfigured) {
|
||||||
|
passwordAttemptLimitHint = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const denyFromConfig = parseFaillockDenyValue(faillockConfigText);
|
||||||
|
if (denyFromConfig >= 0) {
|
||||||
|
passwordAttemptLimitHint = denyFromConfig;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (denyFromPam >= 0) {
|
||||||
|
passwordAttemptLimitHint = denyFromPam;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pam_faillock default deny value when no explicit config is set.
|
||||||
|
passwordAttemptLimitHint = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLikelyLockoutMessage(message) {
|
||||||
|
const lower = (message || "").toLowerCase();
|
||||||
|
return lower.includes("account is locked") || lower.includes("too many") || lower.includes("maximum number of") || lower.includes("auth_err");
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentAuthMessage() {
|
||||||
|
if (GreeterState.pamState === "error")
|
||||||
|
return "Authentication error - try again";
|
||||||
|
if (GreeterState.pamState === "max")
|
||||||
|
return "Too many failed attempts - account may be locked";
|
||||||
|
if (GreeterState.pamState === "fail") {
|
||||||
|
if (passwordAttemptLimitHint > 0) {
|
||||||
|
const attempt = Math.max(1, Math.min(passwordFailureCount, passwordAttemptLimitHint));
|
||||||
|
const remaining = Math.max(passwordAttemptLimitHint - attempt, 0);
|
||||||
|
if (remaining > 0) {
|
||||||
|
return "Incorrect password - attempt " + attempt + " of " + passwordAttemptLimitHint + " (lockout may follow)";
|
||||||
|
}
|
||||||
|
return "Incorrect password - next failures may trigger account lockout";
|
||||||
|
}
|
||||||
|
return "Incorrect password";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAuthFeedback() {
|
||||||
|
GreeterState.pamState = "";
|
||||||
|
authFeedbackMessage = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPasswordSessionTransition(clearSubmitRequest) {
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
|
passwordSessionTransitionRetryCount = 0;
|
||||||
|
if (clearSubmitRequest)
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: GreetdSettings
|
target: GreetdSettings
|
||||||
function onSettingsLoadedChanged() {
|
function onSettingsLoadedChanged() {
|
||||||
if (GreetdSettings.settingsLoaded)
|
if (GreetdSettings.settingsLoaded) {
|
||||||
initWeatherService();
|
initWeatherService();
|
||||||
|
if (isPrimaryScreen) {
|
||||||
|
applyLastSuccessfulUser();
|
||||||
|
finalizeSessionSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRememberLastUserChanged() {
|
||||||
|
if (!isPrimaryScreen)
|
||||||
|
return;
|
||||||
|
if (!GreetdSettings.rememberLastUser && GreetdMemory.lastSuccessfulUser) {
|
||||||
|
GreetdMemory.setLastSuccessfulUser("");
|
||||||
|
}
|
||||||
|
applyLastSuccessfulUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRememberLastSessionChanged() {
|
||||||
|
if (!isPrimaryScreen)
|
||||||
|
return;
|
||||||
|
if (!GreetdSettings.rememberLastSession && GreetdMemory.lastSessionId) {
|
||||||
|
GreetdMemory.setLastSessionId("");
|
||||||
|
}
|
||||||
|
finalizeSessionSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: greetdPamWatcher
|
||||||
|
path: "/etc/pam.d/greetd"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.greetdPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.greetdPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: systemAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/system-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.systemAuthPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.systemAuthPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: commonAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/common-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.commonAuthPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.commonAuthPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: passwordAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/password-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.passwordAuthPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.passwordAuthPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: faillockConfigWatcher
|
||||||
|
path: "/etc/security/faillock.conf"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.faillockConfigText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.faillockConfigText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
initWeatherService();
|
initWeatherService();
|
||||||
|
refreshPasswordAttemptPolicyHint();
|
||||||
|
|
||||||
if (isPrimaryScreen)
|
if (isPrimaryScreen)
|
||||||
applyLastSuccessfulUser();
|
applyLastSuccessfulUser();
|
||||||
@@ -63,15 +332,131 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyLastSuccessfulUser() {
|
function applyLastSuccessfulUser() {
|
||||||
|
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
|
||||||
|
return;
|
||||||
const lastUser = GreetdMemory.lastSuccessfulUser;
|
const lastUser = GreetdMemory.lastSuccessfulUser;
|
||||||
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
|
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
|
||||||
GreeterState.username = lastUser;
|
GreeterState.username = lastUser;
|
||||||
GreeterState.usernameInput = lastUser;
|
GreeterState.usernameInput = lastUser;
|
||||||
GreeterState.showPasswordInput = true;
|
GreeterState.showPasswordInput = true;
|
||||||
PortalService.getGreeterUserProfileImage(lastUser);
|
PortalService.getGreeterUserProfileImage(lastUser);
|
||||||
|
maybeAutoStartExternalAuth();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function submitUsername(rawValue) {
|
||||||
|
const user = (rawValue || "").trim();
|
||||||
|
if (!user)
|
||||||
|
return;
|
||||||
|
if (GreeterState.username !== user) {
|
||||||
|
passwordFailureCount = 0;
|
||||||
|
clearAuthFeedback();
|
||||||
|
externalAuthAutoStartedForUser = "";
|
||||||
|
}
|
||||||
|
GreeterState.username = user;
|
||||||
|
GreeterState.showPasswordInput = true;
|
||||||
|
PortalService.getGreeterUserProfileImage(user);
|
||||||
|
GreeterState.passwordBuffer = "";
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitBufferedPassword() {
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
|
// Some PAM stacks expect an explicit empty response to advance U2F/fprint or fail normally.
|
||||||
|
Greetd.respond(GreeterState.passwordBuffer || "");
|
||||||
|
GreeterState.passwordBuffer = "";
|
||||||
|
inputField.text = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestPasswordSessionTransition() {
|
||||||
|
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
|
||||||
|
if (!passwordSubmitRequested && !hasPasswordBuffer)
|
||||||
|
return;
|
||||||
|
if (cancelingExternalAuthForPassword)
|
||||||
|
return;
|
||||||
|
if (passwordSessionTransitionRetryCount >= maxPasswordSessionTransitionRetries) {
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
|
placeholderDelay.restart();
|
||||||
|
Greetd.cancelSession();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cancelingExternalAuthForPassword = true;
|
||||||
|
passwordSessionTransitionRetryCount = passwordSessionTransitionRetryCount + 1;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
|
Greetd.cancelSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAuthSession(submitPassword) {
|
||||||
|
submitPassword = submitPassword === true;
|
||||||
|
if (!GreeterState.showPasswordInput || !GreeterState.username)
|
||||||
|
return;
|
||||||
|
if (GreeterState.unlocking)
|
||||||
|
return;
|
||||||
|
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
|
||||||
|
if (Greetd.state !== GreetdState.Inactive) {
|
||||||
|
if (pendingPasswordResponse && submitPassword)
|
||||||
|
submitBufferedPassword();
|
||||||
|
else if (submitPassword)
|
||||||
|
passwordSubmitRequested = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cancelingExternalAuthForPassword) {
|
||||||
|
if (submitPassword)
|
||||||
|
passwordSubmitRequested = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!submitPassword && !hasPasswordBuffer && !root.greeterExternalAuthAvailable)
|
||||||
|
return;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
passwordSubmitRequested = submitPassword;
|
||||||
|
awaitingExternalAuth = !submitPassword && !hasPasswordBuffer && root.greeterExternalAuthAvailable;
|
||||||
|
// Included PAM stacks (system-auth/common-auth/password-auth) may still run
|
||||||
|
// biometric/U2F modules before password even when DMS toggles are off.
|
||||||
|
const waitingOnPamExternalBeforePassword = submitPassword && root.greeterPamHasExternalAuth;
|
||||||
|
authTimeout.interval = (awaitingExternalAuth || waitingOnPamExternalBeforePassword) ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
|
Greetd.createSession(GreeterState.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeAutoStartExternalAuth() {
|
||||||
|
if (!GreeterState.showPasswordInput || !GreeterState.username)
|
||||||
|
return;
|
||||||
|
if (!root.greeterExternalAuthAvailable)
|
||||||
|
return;
|
||||||
|
if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive)
|
||||||
|
return;
|
||||||
|
if (passwordSubmitRequested || cancelingExternalAuthForPassword)
|
||||||
|
return;
|
||||||
|
if (GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0)
|
||||||
|
return;
|
||||||
|
if (externalAuthAutoStartedForUser === GreeterState.username)
|
||||||
|
return;
|
||||||
|
|
||||||
|
externalAuthAutoStartedForUser = GreeterState.username;
|
||||||
|
startAuthSession(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExternalAuthPrompt(message, responseRequired) {
|
||||||
|
// Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch).
|
||||||
|
return !responseRequired;
|
||||||
|
}
|
||||||
|
|
||||||
Component.onDestruction: {
|
Component.onDestruction: {
|
||||||
if (weatherInitialized)
|
if (weatherInitialized)
|
||||||
WeatherService.removeRef();
|
WeatherService.removeRef();
|
||||||
@@ -143,10 +528,39 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: greeterWallpaperOverrideFile
|
||||||
|
path: GreetdSettings.greeterWallpaperOverridePath
|
||||||
|
printErrors: false
|
||||||
|
watchChanges: true
|
||||||
|
onLoaded: root.greeterWallpaperOverrideExists = true
|
||||||
|
onLoadFailed: root.greeterWallpaperOverrideExists = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: GreetdSettings
|
||||||
|
function onGreeterWallpaperOverridePathChanged() {
|
||||||
|
if (!GreetdSettings.greeterWallpaperOverridePath) {
|
||||||
|
root.greeterWallpaperOverrideExists = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
greeterWallpaperOverrideFile.reload();
|
||||||
|
}
|
||||||
|
function onGreeterWallpaperPathChanged() {
|
||||||
|
if (!GreetdSettings.greeterWallpaperPath) {
|
||||||
|
root.greeterWallpaperOverrideExists = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
greeterWallpaperOverrideFile.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DankBackdrop {
|
DankBackdrop {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
screenName: root.screenName
|
screenName: root.screenName
|
||||||
visible: {
|
visible: {
|
||||||
|
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
|
||||||
|
return false;
|
||||||
var _ = SessionData.perMonitorWallpaper;
|
var _ = SessionData.perMonitorWallpaper;
|
||||||
var __ = SessionData.monitorWallpapers;
|
var __ = SessionData.monitorWallpapers;
|
||||||
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
||||||
@@ -159,12 +573,14 @@ Item {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: {
|
source: {
|
||||||
|
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
|
||||||
|
return encodeFileUrl(GreetdSettings.greeterWallpaperOverridePath);
|
||||||
var _ = SessionData.perMonitorWallpaper;
|
var _ = SessionData.perMonitorWallpaper;
|
||||||
var __ = SessionData.monitorWallpapers;
|
var __ = SessionData.monitorWallpapers;
|
||||||
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
||||||
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
|
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
|
||||||
}
|
}
|
||||||
fillMode: Theme.getFillMode(GreetdSettings.wallpaperFillMode)
|
fillMode: Theme.getFillMode(GreetdSettings.getEffectiveWallpaperFillMode())
|
||||||
smooth: true
|
smooth: true
|
||||||
asynchronous: false
|
asynchronous: false
|
||||||
cache: true
|
cache: true
|
||||||
@@ -327,10 +743,7 @@ Item {
|
|||||||
anchors.top: clockContainer.bottom
|
anchors.top: clockContainer.bottom
|
||||||
anchors.topMargin: 4
|
anchors.topMargin: 4
|
||||||
text: {
|
text: {
|
||||||
if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) {
|
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat());
|
||||||
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.lockDateFormat);
|
|
||||||
}
|
|
||||||
return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
|
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
color: "white"
|
color: "white"
|
||||||
@@ -399,6 +812,9 @@ Item {
|
|||||||
if (GreeterState.showPasswordInput && revealButton.visible) {
|
if (GreeterState.showPasswordInput && revealButton.visible) {
|
||||||
margin += revealButton.width;
|
margin += revealButton.width;
|
||||||
}
|
}
|
||||||
|
if (externalAuthButton.visible) {
|
||||||
|
margin += externalAuthButton.width;
|
||||||
|
}
|
||||||
if (virtualKeyboardButton.visible) {
|
if (virtualKeyboardButton.visible) {
|
||||||
margin += virtualKeyboardButton.width;
|
margin += virtualKeyboardButton.width;
|
||||||
}
|
}
|
||||||
@@ -415,21 +831,18 @@ Item {
|
|||||||
return;
|
return;
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
GreeterState.passwordBuffer = text;
|
GreeterState.passwordBuffer = text;
|
||||||
|
if (!text || text.length === 0)
|
||||||
|
root.passwordSubmitRequested = false;
|
||||||
} else {
|
} else {
|
||||||
GreeterState.usernameInput = text;
|
GreeterState.usernameInput = text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
if (Greetd.state === GreetdState.Inactive && GreeterState.username) {
|
root.startAuthSession(true);
|
||||||
Greetd.createSession(GreeterState.username);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (text.trim()) {
|
if (text.trim()) {
|
||||||
GreeterState.username = text.trim();
|
root.submitUsername(text);
|
||||||
GreeterState.showPasswordInput = true;
|
|
||||||
PortalService.getGreeterUserProfileImage(GreeterState.username);
|
|
||||||
GreeterState.passwordBuffer = "";
|
|
||||||
syncingFromState = true;
|
syncingFromState = true;
|
||||||
text = "";
|
text = "";
|
||||||
syncingFromState = false;
|
syncingFromState = false;
|
||||||
@@ -461,14 +874,14 @@ Item {
|
|||||||
|
|
||||||
anchors.left: lockIcon.right
|
anchors.left: lockIcon.right
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))
|
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))))
|
||||||
anchors.rightMargin: 2
|
anchors.rightMargin: 2
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: {
|
text: {
|
||||||
if (GreeterState.unlocking) {
|
if (GreeterState.unlocking) {
|
||||||
return "Logging in...";
|
return "Logging in...";
|
||||||
}
|
}
|
||||||
if (Greetd.state !== GreetdState.Inactive) {
|
if (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse) {
|
||||||
return "Authenticating...";
|
return "Authenticating...";
|
||||||
}
|
}
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
@@ -476,7 +889,7 @@ Item {
|
|||||||
}
|
}
|
||||||
return "Username...";
|
return "Username...";
|
||||||
}
|
}
|
||||||
color: GreeterState.unlocking ? Theme.primary : (Greetd.state !== GreetdState.Inactive ? Theme.primary : Theme.outline)
|
color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
|
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
|
||||||
|
|
||||||
@@ -498,7 +911,7 @@ Item {
|
|||||||
StyledText {
|
StyledText {
|
||||||
anchors.left: lockIcon.right
|
anchors.left: lockIcon.right
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))
|
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))))
|
||||||
anchors.rightMargin: 2
|
anchors.rightMargin: 2
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: {
|
text: {
|
||||||
@@ -528,15 +941,27 @@ Item {
|
|||||||
DankActionButton {
|
DankActionButton {
|
||||||
id: revealButton
|
id: revealButton
|
||||||
|
|
||||||
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
|
anchors.right: externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))
|
||||||
anchors.rightMargin: 0
|
anchors.rightMargin: 0
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: parent.showPassword ? "visibility_off" : "visibility"
|
iconName: parent.showPassword ? "visibility_off" : "visibility"
|
||||||
buttonSize: 32
|
buttonSize: 32
|
||||||
visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
|
visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
enabled: visible
|
enabled: visible
|
||||||
onClicked: parent.showPassword = !parent.showPassword
|
onClicked: parent.showPassword = !parent.showPassword
|
||||||
}
|
}
|
||||||
|
DankActionButton {
|
||||||
|
id: externalAuthButton
|
||||||
|
|
||||||
|
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
|
||||||
|
anchors.rightMargin: 0
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: root.greeterPamHasFprint ? "fingerprint" : "key"
|
||||||
|
buttonSize: 32
|
||||||
|
visible: GreeterState.showPasswordInput && root.greeterExternalAuthAvailable && GreeterState.passwordBuffer.length === 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
|
enabled: visible
|
||||||
|
onClicked: root.startAuthSession(false)
|
||||||
|
}
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
id: virtualKeyboardButton
|
id: virtualKeyboardButton
|
||||||
|
|
||||||
@@ -545,7 +970,7 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: "keyboard"
|
iconName: "keyboard"
|
||||||
buttonSize: 32
|
buttonSize: 32
|
||||||
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
|
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
enabled: visible
|
enabled: visible
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (keyboard_controller.isKeyboardActive) {
|
if (keyboard_controller.isKeyboardActive) {
|
||||||
@@ -564,19 +989,14 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: "keyboard_return"
|
iconName: "keyboard_return"
|
||||||
buttonSize: 36
|
buttonSize: 36
|
||||||
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
|
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
enabled: true
|
enabled: true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
if (GreeterState.username) {
|
root.startAuthSession(true);
|
||||||
Greetd.createSession(GreeterState.username);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (inputField.text.trim()) {
|
if (inputField.text.trim()) {
|
||||||
GreeterState.username = inputField.text.trim();
|
root.submitUsername(inputField.text);
|
||||||
GreeterState.showPasswordInput = true;
|
|
||||||
PortalService.getGreeterUserProfileImage(GreeterState.username);
|
|
||||||
GreeterState.passwordBuffer = "";
|
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -601,20 +1021,16 @@ Item {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 20
|
Layout.preferredHeight: 38
|
||||||
Layout.topMargin: -Theme.spacingS
|
Layout.topMargin: -Theme.spacingS
|
||||||
Layout.bottomMargin: -Theme.spacingS
|
Layout.bottomMargin: -Theme.spacingS
|
||||||
text: {
|
text: root.authFeedbackMessage
|
||||||
if (GreeterState.pamState === "error")
|
|
||||||
return "Authentication error - try again";
|
|
||||||
if (GreeterState.pamState === "fail")
|
|
||||||
return "Incorrect password";
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
color: Theme.error
|
color: Theme.error
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
opacity: GreeterState.pamState !== "" ? 1 : 0
|
wrapMode: Text.WordWrap
|
||||||
|
maximumLineCount: 2
|
||||||
|
opacity: root.authFeedbackMessage !== "" ? 1 : 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
@@ -667,6 +1083,7 @@ Item {
|
|||||||
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
|
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
|
||||||
onClicked: {
|
onClicked: {
|
||||||
GreeterState.reset();
|
GreeterState.reset();
|
||||||
|
root.externalAuthAutoStartedForUser = "";
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
PortalService.profileImage = "";
|
PortalService.profileImage = "";
|
||||||
}
|
}
|
||||||
@@ -1029,9 +1446,11 @@ Item {
|
|||||||
return;
|
return;
|
||||||
if (!GreetdMemory.memoryReady)
|
if (!GreetdMemory.memoryReady)
|
||||||
return;
|
return;
|
||||||
|
if (!GreetdSettings.settingsLoaded)
|
||||||
|
return;
|
||||||
|
|
||||||
const savedSession = GreetdMemory.lastSessionId;
|
const savedSession = GreetdSettings.rememberLastSession ? GreetdMemory.lastSessionId : "";
|
||||||
if (savedSession) {
|
if (savedSession && GreetdSettings.rememberLastSession) {
|
||||||
for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
|
for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
|
||||||
if (GreeterState.sessionPaths[i] === savedSession) {
|
if (GreeterState.sessionPaths[i] === savedSession) {
|
||||||
GreeterState.currentSessionIndex = i;
|
GreeterState.currentSessionIndex = i;
|
||||||
@@ -1164,44 +1583,151 @@ Item {
|
|||||||
|
|
||||||
function onAuthMessage(message, error, responseRequired, echoResponse) {
|
function onAuthMessage(message, error, responseRequired, echoResponse) {
|
||||||
if (responseRequired) {
|
if (responseRequired) {
|
||||||
Greetd.respond(GreeterState.passwordBuffer);
|
cancelingExternalAuthForPassword = false;
|
||||||
GreeterState.passwordBuffer = "";
|
passwordSessionTransitionRetryCount = 0;
|
||||||
inputField.text = "";
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = true;
|
||||||
|
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
|
||||||
|
if (!passwordSubmitRequested && hasPasswordBuffer)
|
||||||
|
passwordSubmitRequested = true;
|
||||||
|
if (passwordSubmitRequested && !root.submitBufferedPassword())
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
if (passwordSubmitRequested || hasPasswordBuffer) {
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
|
} else {
|
||||||
|
authTimeout.stop();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!error)
|
pendingPasswordResponse = false;
|
||||||
Greetd.respond("");
|
const externalPrompt = root.isExternalAuthPrompt(message, responseRequired);
|
||||||
|
if (!passwordSubmitRequested)
|
||||||
|
awaitingExternalAuth = root.greeterExternalAuthAvailable && externalPrompt;
|
||||||
|
if (awaitingExternalAuth || (passwordSubmitRequested && externalPrompt && root.greeterPamHasExternalAuth))
|
||||||
|
authTimeout.interval = externalAuthTimeoutMs;
|
||||||
|
else
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
|
Greetd.respond("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStateChanged() {
|
||||||
|
if (Greetd.state === GreetdState.Inactive) {
|
||||||
|
const resumePasswordSubmit = cancelingExternalAuthForPassword && passwordSubmitRequested;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
|
if (resumePasswordSubmit) {
|
||||||
|
Qt.callLater(function() {
|
||||||
|
root.startAuthSession(true);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReadyToLaunch() {
|
function onReadyToLaunch() {
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
|
passwordFailureCount = 0;
|
||||||
|
clearAuthFeedback();
|
||||||
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
|
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
|
||||||
const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
|
const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
|
||||||
if (!sessionCmd) {
|
if (!sessionCmd) {
|
||||||
GreeterState.pamState = "error";
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GreeterState.unlocking = true;
|
GreeterState.unlocking = true;
|
||||||
launchTimeout.restart();
|
launchTimeout.restart();
|
||||||
GreetdMemory.setLastSessionId(sessionPath);
|
if (GreetdSettings.rememberLastSession) {
|
||||||
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
|
GreetdMemory.setLastSessionId(sessionPath);
|
||||||
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
|
} else if (GreetdMemory.lastSessionId) {
|
||||||
|
GreetdMemory.setLastSessionId("");
|
||||||
|
}
|
||||||
|
if (GreetdSettings.rememberLastUser) {
|
||||||
|
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
|
||||||
|
} else if (GreetdMemory.lastSuccessfulUser) {
|
||||||
|
GreetdMemory.setLastSuccessfulUser("");
|
||||||
|
}
|
||||||
|
pendingLaunchCommand = sessionCmd;
|
||||||
|
pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"];
|
||||||
|
memoryFlushTimer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAuthFailure(message) {
|
function onAuthFailure(message) {
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
launchTimeout.stop();
|
launchTimeout.stop();
|
||||||
GreeterState.unlocking = false;
|
GreeterState.unlocking = false;
|
||||||
GreeterState.pamState = "fail";
|
if (isLikelyLockoutMessage(message)) {
|
||||||
|
GreeterState.pamState = "max";
|
||||||
|
} else {
|
||||||
|
GreeterState.pamState = "fail";
|
||||||
|
passwordFailureCount = passwordFailureCount + 1;
|
||||||
|
}
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
GreeterState.passwordBuffer = "";
|
GreeterState.passwordBuffer = "";
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
|
Greetd.cancelSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onError(error) {
|
function onError(error) {
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
launchTimeout.stop();
|
launchTimeout.stop();
|
||||||
GreeterState.unlocking = false;
|
GreeterState.unlocking = false;
|
||||||
GreeterState.pamState = "error";
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
|
GreeterState.passwordBuffer = "";
|
||||||
|
inputField.text = "";
|
||||||
|
placeholderDelay.restart();
|
||||||
|
Greetd.cancelSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: memoryFlushTimer
|
||||||
|
interval: memoryFlushDelayMs
|
||||||
|
onTriggered: {
|
||||||
|
if (!pendingLaunchCommand)
|
||||||
|
return;
|
||||||
|
const sessionCommand = pendingLaunchCommand;
|
||||||
|
const launchEnv = pendingLaunchEnv;
|
||||||
|
pendingLaunchCommand = "";
|
||||||
|
pendingLaunchEnv = [];
|
||||||
|
Greetd.launch(sessionCommand.split(" "), launchEnv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: authTimeout
|
||||||
|
interval: defaultAuthTimeoutMs
|
||||||
|
onTriggered: {
|
||||||
|
if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive)
|
||||||
|
return;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
GreeterState.passwordBuffer = "";
|
GreeterState.passwordBuffer = "";
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
@@ -1215,8 +1741,11 @@ Item {
|
|||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (!GreeterState.unlocking)
|
if (!GreeterState.unlocking)
|
||||||
return;
|
return;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
GreeterState.unlocking = false;
|
GreeterState.unlocking = false;
|
||||||
GreeterState.pamState = "error";
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
Greetd.cancelSession();
|
Greetd.cancelSession();
|
||||||
}
|
}
|
||||||
@@ -1225,7 +1754,7 @@ Item {
|
|||||||
Timer {
|
Timer {
|
||||||
id: placeholderDelay
|
id: placeholderDelay
|
||||||
interval: 4000
|
interval: 4000
|
||||||
onTriggered: GreeterState.pamState = ""
|
onTriggered: clearAuthFeedback()
|
||||||
}
|
}
|
||||||
|
|
||||||
LockPowerMenu {
|
LockPowerMenu {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the
|
|||||||
- **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc.
|
- **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc.
|
||||||
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
|
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
|
||||||
- **Session Memory**: Remembers last selected session and user
|
- **Session Memory**: Remembers last selected session and user
|
||||||
|
- Can be disabled via `settings.json` keys: `greeterRememberLastSession` and `greeterRememberLastUser`
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -212,6 +213,7 @@ dms-greeter --command hyprland
|
|||||||
dms-greeter --command sway
|
dms-greeter --command sway
|
||||||
dms-greeter --command mangowc
|
dms-greeter --command mangowc
|
||||||
dms-greeter --command niri -C /path/to/custom-niri.kdl
|
dms-greeter --command niri -C /path/to/custom-niri.kdl
|
||||||
|
dms-greeter --command niri --remember-last-user false --remember-last-session false
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure greetd to use it in `/etc/greetd/config.toml`:
|
Configure greetd to use it in `/etc/greetd/config.toml`:
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ COMPOSITOR=""
|
|||||||
COMPOSITOR_CONFIG=""
|
COMPOSITOR_CONFIG=""
|
||||||
DMS_PATH="dms-greeter"
|
DMS_PATH="dms-greeter"
|
||||||
CACHE_DIR="/var/cache/dms-greeter"
|
CACHE_DIR="/var/cache/dms-greeter"
|
||||||
|
REMEMBER_LAST_SESSION=""
|
||||||
|
REMEMBER_LAST_USER=""
|
||||||
|
DEBUG_MODE=0
|
||||||
|
|
||||||
show_help() {
|
show_help() {
|
||||||
cat << EOF
|
cat << EOF
|
||||||
@@ -22,6 +25,15 @@ Options:
|
|||||||
(default: dms-greeter)
|
(default: dms-greeter)
|
||||||
--cache-dir PATH Cache directory for greeter data
|
--cache-dir PATH Cache directory for greeter data
|
||||||
(default: /var/cache/dms-greeter)
|
(default: /var/cache/dms-greeter)
|
||||||
|
--remember-last-session BOOL
|
||||||
|
Persist selected session to greeter memory
|
||||||
|
(BOOL: true/false, default: from settings.json)
|
||||||
|
--remember-last-user BOOL
|
||||||
|
Persist last successful username to greeter memory
|
||||||
|
(BOOL: true/false, default: from settings.json)
|
||||||
|
--no-save-session Alias for --remember-last-session false
|
||||||
|
--no-save-username Alias for --remember-last-user false
|
||||||
|
--debug Enable verbose startup logging to stderr
|
||||||
-h, --help Show this help message
|
-h, --help Show this help message
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -30,6 +42,7 @@ Examples:
|
|||||||
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
|
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
|
||||||
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
|
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
|
||||||
dms-greeter --command niri --cache-dir /tmp/dmsgreeter
|
dms-greeter --command niri --cache-dir /tmp/dmsgreeter
|
||||||
|
dms-greeter --command niri --no-save-session --no-save-username
|
||||||
dms-greeter --command mango
|
dms-greeter --command mango
|
||||||
dms-greeter --command labwc
|
dms-greeter --command labwc
|
||||||
EOF
|
EOF
|
||||||
@@ -43,6 +56,41 @@ require_command() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalize_bool_flag() {
|
||||||
|
local flag_name="$1"
|
||||||
|
local value="$2"
|
||||||
|
local normalized="${value,,}"
|
||||||
|
|
||||||
|
case "$normalized" in
|
||||||
|
1|true|yes|on)
|
||||||
|
echo "1"
|
||||||
|
;;
|
||||||
|
0|false|no|off)
|
||||||
|
echo "0"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: $flag_name must be true/false (or 1/0, yes/no, on/off)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
exec_compositor() {
|
||||||
|
local log_tag="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
if [[ "$DEBUG_MODE" == "1" ]]; then
|
||||||
|
exec "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v systemd-cat >/dev/null 2>&1; then
|
||||||
|
exec "$@" > >(systemd-cat -t "dms-greeter/$log_tag" -p info) 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local log_file="$CACHE_DIR/$log_tag.log"
|
||||||
|
exec "$@" >> "$log_file" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
--command)
|
--command)
|
||||||
@@ -61,6 +109,26 @@ while [[ $# -gt 0 ]]; do
|
|||||||
CACHE_DIR="$2"
|
CACHE_DIR="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--remember-last-session)
|
||||||
|
REMEMBER_LAST_SESSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--remember-last-user)
|
||||||
|
REMEMBER_LAST_USER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--no-save-session)
|
||||||
|
REMEMBER_LAST_SESSION="0"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--no-save-username)
|
||||||
|
REMEMBER_LAST_USER="0"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--debug)
|
||||||
|
DEBUG_MODE=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
show_help
|
show_help
|
||||||
exit 0
|
exit 0
|
||||||
@@ -111,9 +179,58 @@ export QT_QPA_PLATFORM=wayland
|
|||||||
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
||||||
export EGL_PLATFORM=gbm
|
export EGL_PLATFORM=gbm
|
||||||
export DMS_RUN_GREETER=1
|
export DMS_RUN_GREETER=1
|
||||||
|
|
||||||
|
ensure_cache_tree() {
|
||||||
|
local base="$1"
|
||||||
|
mkdir -p "$base/.local/state" "$base/.local/share" "$base/.cache"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! ensure_cache_tree "$CACHE_DIR" 2>/dev/null; then
|
||||||
|
FALLBACK_CACHE_DIR="/tmp/dms-greeter-${UID:-$(id -u)}"
|
||||||
|
echo "Warning: cache directory '$CACHE_DIR' is not writable; falling back to '$FALLBACK_CACHE_DIR'" >&2
|
||||||
|
CACHE_DIR="$FALLBACK_CACHE_DIR"
|
||||||
|
if ! ensure_cache_tree "$CACHE_DIR"; then
|
||||||
|
echo "Error: failed to initialize fallback cache directory '$CACHE_DIR'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
export DMS_GREET_CFG_DIR="$CACHE_DIR"
|
export DMS_GREET_CFG_DIR="$CACHE_DIR"
|
||||||
|
|
||||||
mkdir -p "$CACHE_DIR"
|
if [[ -n "$REMEMBER_LAST_SESSION" ]]; then
|
||||||
|
DMS_GREET_REMEMBER_LAST_SESSION=$(normalize_bool_flag "--remember-last-session" "$REMEMBER_LAST_SESSION")
|
||||||
|
export DMS_GREET_REMEMBER_LAST_SESSION
|
||||||
|
if [[ "$DMS_GREET_REMEMBER_LAST_SESSION" == "1" ]]; then
|
||||||
|
DMS_SAVE_SESSION=true
|
||||||
|
else
|
||||||
|
DMS_SAVE_SESSION=false
|
||||||
|
fi
|
||||||
|
export DMS_SAVE_SESSION
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$REMEMBER_LAST_USER" ]]; then
|
||||||
|
DMS_GREET_REMEMBER_LAST_USER=$(normalize_bool_flag "--remember-last-user" "$REMEMBER_LAST_USER")
|
||||||
|
export DMS_GREET_REMEMBER_LAST_USER
|
||||||
|
if [[ "$DMS_GREET_REMEMBER_LAST_USER" == "1" ]]; then
|
||||||
|
DMS_SAVE_USERNAME=true
|
||||||
|
else
|
||||||
|
DMS_SAVE_USERNAME=false
|
||||||
|
fi
|
||||||
|
export DMS_SAVE_USERNAME
|
||||||
|
fi
|
||||||
|
|
||||||
|
export HOME="$CACHE_DIR"
|
||||||
|
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
|
||||||
|
export XDG_DATA_HOME="$CACHE_DIR/.local/share"
|
||||||
|
export XDG_CACHE_HOME="$CACHE_DIR/.cache"
|
||||||
|
|
||||||
|
# Keep greeter VT clean by default; callers can override via env or --debug.
|
||||||
|
if [[ -z "${RUST_LOG:-}" ]]; then
|
||||||
|
export RUST_LOG=warn
|
||||||
|
fi
|
||||||
|
if [[ -z "${NIRI_LOG:-}" ]]; then
|
||||||
|
export NIRI_LOG=warn
|
||||||
|
fi
|
||||||
|
|
||||||
if command -v qs >/dev/null 2>&1; then
|
if command -v qs >/dev/null 2>&1; then
|
||||||
QS_BIN="qs"
|
QS_BIN="qs"
|
||||||
@@ -130,7 +247,9 @@ if [[ "$DMS_PATH" == /* ]]; then
|
|||||||
else
|
else
|
||||||
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
|
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
|
||||||
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
|
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
|
||||||
echo "Located DMS config at: $RESOLVED_PATH" >&2
|
if [[ "$DEBUG_MODE" == "1" ]]; then
|
||||||
|
echo "Located DMS config at: $RESOLVED_PATH" >&2
|
||||||
|
fi
|
||||||
QS_CMD="$QS_BIN -p $RESOLVED_PATH"
|
QS_CMD="$QS_BIN -p $RESOLVED_PATH"
|
||||||
else
|
else
|
||||||
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
|
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
|
||||||
@@ -192,7 +311,7 @@ NIRI_EOF
|
|||||||
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
|
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
|
||||||
NIRI_EOF
|
NIRI_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
exec niri -c "$COMPOSITOR_CONFIG"
|
exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
hyprland)
|
hyprland)
|
||||||
@@ -222,9 +341,9 @@ HYPRLAND_EOF
|
|||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
if command -v start-hyprland >/dev/null 2>&1; then
|
if command -v start-hyprland >/dev/null 2>&1; then
|
||||||
exec start-hyprland -- --config "$COMPOSITOR_CONFIG"
|
exec_compositor "hyprland" start-hyprland -- --config "$COMPOSITOR_CONFIG"
|
||||||
else
|
else
|
||||||
exec Hyprland -c "$COMPOSITOR_CONFIG"
|
exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
@@ -245,7 +364,7 @@ exec "$QS_CMD; swaymsg exit"
|
|||||||
SWAY_EOF
|
SWAY_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
exec sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
|
exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
scroll)
|
scroll)
|
||||||
@@ -265,7 +384,7 @@ exec "$QS_CMD; scrollmsg exit"
|
|||||||
SCROLL_EOF
|
SCROLL_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
exec scroll -c "$COMPOSITOR_CONFIG"
|
exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
miracle|miracle-wm)
|
miracle|miracle-wm)
|
||||||
@@ -285,24 +404,24 @@ exec "$QS_CMD; miraclemsg exit"
|
|||||||
MIRACLE_EOF
|
MIRACLE_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
exec miracle-wm -c "$COMPOSITOR_CONFIG"
|
exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
labwc)
|
labwc)
|
||||||
require_command "labwc"
|
require_command "labwc"
|
||||||
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
||||||
exec labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
|
exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
|
||||||
else
|
else
|
||||||
exec labwc --session "$QS_CMD"
|
exec_compositor "labwc" labwc --session "$QS_CMD"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
mango|mangowc)
|
mango|mangowc)
|
||||||
require_command "mango"
|
require_command "mango"
|
||||||
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
||||||
exec mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
|
exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
|
||||||
else
|
else
|
||||||
exec mango -s "$QS_CMD && mmsg -d quit"
|
exec_compositor "mango" mango -s "$QS_CMD && mmsg -d quit"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
|||||||
@@ -755,7 +755,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (!demoMode && !pam.passwd.active && !pam.u2fPending) {
|
if (!demoMode && !root.unlocking && !pam.passwd.active && !pam.u2fPending) {
|
||||||
pam.passwd.start();
|
pam.passwd.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -764,6 +764,11 @@ Item {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (root.unlocking) {
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape) {
|
||||||
if (pam.u2fPending) {
|
if (pam.u2fPending) {
|
||||||
pam.cancelU2fPending();
|
pam.cancelU2fPending();
|
||||||
@@ -1017,7 +1022,7 @@ Item {
|
|||||||
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
|
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
|
||||||
enabled: !demoMode
|
enabled: !demoMode
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!demoMode && !pam.u2fPending) {
|
if (!demoMode && !root.unlocking && !pam.u2fPending) {
|
||||||
pam.passwd.start();
|
pam.passwd.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1626,6 +1631,7 @@ Item {
|
|||||||
onStateChanged: {
|
onStateChanged: {
|
||||||
root.pamState = state;
|
root.pamState = state;
|
||||||
if (state !== "") {
|
if (state !== "") {
|
||||||
|
root.unlocking = false;
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
passwordField.text = "";
|
passwordField.text = "";
|
||||||
root.passwordBuffer = "";
|
root.passwordBuffer = "";
|
||||||
@@ -1641,6 +1647,15 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: pam
|
||||||
|
|
||||||
|
function onUnlockInProgressChanged() {
|
||||||
|
if (!pam.unlockInProgress && root.unlocking)
|
||||||
|
root.unlocking = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Binding {
|
Binding {
|
||||||
target: pam
|
target: pam
|
||||||
property: "buffer"
|
property: "buffer"
|
||||||
|
|||||||
@@ -25,6 +25,29 @@ Scope {
|
|||||||
signal flashMsg
|
signal flashMsg
|
||||||
signal unlockRequested
|
signal unlockRequested
|
||||||
|
|
||||||
|
function resetAuthFlows(): void {
|
||||||
|
passwd.abort();
|
||||||
|
fprint.abort();
|
||||||
|
u2f.abort();
|
||||||
|
errorRetry.running = false;
|
||||||
|
u2fErrorRetry.running = false;
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
passwdActiveTimeout.running = false;
|
||||||
|
unlockRequestTimeout.running = false;
|
||||||
|
u2fPending = false;
|
||||||
|
u2fState = "";
|
||||||
|
unlockInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recoverFromAuthStall(newState: string): void {
|
||||||
|
resetAuthFlows();
|
||||||
|
state = newState;
|
||||||
|
flashMsg();
|
||||||
|
stateReset.restart();
|
||||||
|
fprint.checkAvail();
|
||||||
|
u2f.checkAvail();
|
||||||
|
}
|
||||||
|
|
||||||
function completeUnlock(): void {
|
function completeUnlock(): void {
|
||||||
if (!unlockInProgress) {
|
if (!unlockInProgress) {
|
||||||
unlockInProgress = true;
|
unlockInProgress = true;
|
||||||
@@ -36,6 +59,7 @@ Scope {
|
|||||||
u2fPendingTimeout.running = false;
|
u2fPendingTimeout.running = false;
|
||||||
u2fPending = false;
|
u2fPending = false;
|
||||||
u2fState = "";
|
u2fState = "";
|
||||||
|
unlockRequestTimeout.restart();
|
||||||
unlockRequested();
|
unlockRequested();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +90,13 @@ Scope {
|
|||||||
printErrors: false
|
printErrors: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: loginConfigWatcher
|
||||||
|
|
||||||
|
path: "/etc/pam.d/login"
|
||||||
|
printErrors: false
|
||||||
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: u2fConfigWatcher
|
id: u2fConfigWatcher
|
||||||
|
|
||||||
@@ -77,7 +108,7 @@ Scope {
|
|||||||
id: passwd
|
id: passwd
|
||||||
|
|
||||||
config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
|
config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
|
||||||
configDirectory: dankshellConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
configDirectory: dankshellConfigWatcher.loaded || loginConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
||||||
|
|
||||||
onMessageChanged: {
|
onMessageChanged: {
|
||||||
if (message.startsWith("The account is locked"))
|
if (message.startsWith("The account is locked"))
|
||||||
@@ -102,6 +133,13 @@ Scope {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unlockRequestTimeout.running = false;
|
||||||
|
root.unlockInProgress = false;
|
||||||
|
root.u2fPending = false;
|
||||||
|
root.u2fState = "";
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
u2f.abort();
|
||||||
|
|
||||||
if (res === PamResult.Error)
|
if (res === PamResult.Error)
|
||||||
root.state = "error";
|
root.state = "error";
|
||||||
else if (res === PamResult.MaxTries)
|
else if (res === PamResult.MaxTries)
|
||||||
@@ -114,6 +152,18 @@ Scope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: passwd
|
||||||
|
|
||||||
|
function onActiveChanged() {
|
||||||
|
if (passwd.active) {
|
||||||
|
passwdActiveTimeout.restart();
|
||||||
|
} else {
|
||||||
|
passwdActiveTimeout.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PamContext {
|
PamContext {
|
||||||
id: fprint
|
id: fprint
|
||||||
|
|
||||||
@@ -241,7 +291,7 @@ Scope {
|
|||||||
Process {
|
Process {
|
||||||
id: availProc
|
id: availProc
|
||||||
|
|
||||||
command: ["sh", "-c", "fprintd-list $USER"]
|
command: ["sh", "-c", "fprintd-list \"${USER:-$(id -un)}\""]
|
||||||
onExited: code => {
|
onExited: code => {
|
||||||
fprint.available = code === 0;
|
fprint.available = code === 0;
|
||||||
fprint.checkAvail();
|
fprint.checkAvail();
|
||||||
@@ -279,6 +329,26 @@ Scope {
|
|||||||
onTriggered: root.cancelU2fPending()
|
onTriggered: root.cancelU2fPending()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: passwdActiveTimeout
|
||||||
|
|
||||||
|
interval: 15000
|
||||||
|
onTriggered: {
|
||||||
|
if (passwd.active)
|
||||||
|
root.recoverFromAuthStall("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: unlockRequestTimeout
|
||||||
|
|
||||||
|
interval: 8000
|
||||||
|
onTriggered: {
|
||||||
|
if (root.unlockInProgress)
|
||||||
|
root.recoverFromAuthStall("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: stateReset
|
id: stateReset
|
||||||
|
|
||||||
@@ -308,17 +378,9 @@ Scope {
|
|||||||
root.u2fState = "";
|
root.u2fState = "";
|
||||||
root.u2fPending = false;
|
root.u2fPending = false;
|
||||||
root.lockMessage = "";
|
root.lockMessage = "";
|
||||||
root.unlockInProgress = false;
|
root.resetAuthFlows();
|
||||||
} else {
|
} else {
|
||||||
fprint.abort();
|
root.resetAuthFlows();
|
||||||
passwd.abort();
|
|
||||||
u2f.abort();
|
|
||||||
errorRetry.running = false;
|
|
||||||
u2fErrorRetry.running = false;
|
|
||||||
u2fPendingTimeout.running = false;
|
|
||||||
root.u2fPending = false;
|
|
||||||
root.u2fState = "";
|
|
||||||
root.unlockInProgress = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,6 +400,7 @@ Scope {
|
|||||||
u2f.abort();
|
u2f.abort();
|
||||||
u2fErrorRetry.running = false;
|
u2fErrorRetry.running = false;
|
||||||
u2fPendingTimeout.running = false;
|
u2fPendingTimeout.running = false;
|
||||||
|
unlockRequestTimeout.running = false;
|
||||||
root.u2fPending = false;
|
root.u2fPending = false;
|
||||||
root.u2fState = "";
|
root.u2fState = "";
|
||||||
u2f.checkAvail();
|
u2f.checkAvail();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ DankPopout {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
layerNamespace: "dms:notification-center-popout"
|
layerNamespace: "dms:notification-center-popout"
|
||||||
fullHeightSurface: false
|
fullHeightSurface: true
|
||||||
|
|
||||||
property bool notificationHistoryVisible: false
|
property bool notificationHistoryVisible: false
|
||||||
property var triggerScreen: null
|
property var triggerScreen: null
|
||||||
|
|||||||
@@ -108,6 +108,13 @@ QtObject {
|
|||||||
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
|
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _isFocusedScreen() {
|
||||||
|
if (!SettingsData.notificationFocusedMonitor)
|
||||||
|
return true;
|
||||||
|
const focused = CompositorService.getFocusedScreen();
|
||||||
|
return focused && manager.modelData && focused.name === manager.modelData.name;
|
||||||
|
}
|
||||||
|
|
||||||
function _sync(newWrappers) {
|
function _sync(newWrappers) {
|
||||||
for (const p of popupWindows.slice()) {
|
for (const p of popupWindows.slice()) {
|
||||||
if (!_isValidWindow(p) || p.exiting)
|
if (!_isValidWindow(p) || p.exiting)
|
||||||
@@ -118,7 +125,7 @@ QtObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const w of newWrappers) {
|
for (const w of newWrappers) {
|
||||||
if (w && !_hasWindowFor(w))
|
if (w && !_hasWindowFor(w) && _isFocusedScreen())
|
||||||
_insertAtTop(w);
|
_insertAtTop(w);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -909,6 +909,9 @@ Singleton {
|
|||||||
case "dwl":
|
case "dwl":
|
||||||
DwlService.generateOutputsConfig(outputsData);
|
DwlService.generateOutputsConfig(outputsData);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
WlrOutputService.applyOutputsConfig(outputsData, outputs);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -417,6 +417,15 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("Focused monitor only")
|
||||||
|
description: I18n.tr("Show notifications only on the currently focused monitor")
|
||||||
|
visible: parent.componentId === "notifications"
|
||||||
|
checked: SettingsData.notificationFocusedMonitor
|
||||||
|
onToggled: checked => SettingsData.set("notificationFocusedMonitor", checked)
|
||||||
|
}
|
||||||
|
|
||||||
DankToggle {
|
DankToggle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: I18n.tr("Show on Last Display")
|
text: I18n.tr("Show on Last Display")
|
||||||
|
|||||||
@@ -160,6 +160,16 @@ Item {
|
|||||||
onToggled: checked => SettingsData.set("dockGroupByApp", checked)
|
onToggled: checked => SettingsData.set("dockGroupByApp", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "dockRestoreSpecialWorkspaceOnClick"
|
||||||
|
tags: ["dock", "hyprland", "special", "workspace", "restore"]
|
||||||
|
text: I18n.tr("Restore Special Workspace Windows")
|
||||||
|
description: I18n.tr("When clicking a dock window in a Hyprland special workspace, bring that special workspace back before focusing the window")
|
||||||
|
checked: SettingsData.dockRestoreSpecialWorkspaceOnClick
|
||||||
|
visible: CompositorService.isHyprland
|
||||||
|
onToggled: checked => SettingsData.set("dockRestoreSpecialWorkspaceOnClick", checked)
|
||||||
|
}
|
||||||
|
|
||||||
SettingsButtonGroupRow {
|
SettingsButtonGroupRow {
|
||||||
settingKey: "dockIndicatorStyle"
|
settingKey: "dockIndicatorStyle"
|
||||||
tags: ["dock", "indicator", "style", "circle", "line"]
|
tags: ["dock", "indicator", "style", "circle", "line"]
|
||||||
|
|||||||
@@ -0,0 +1,743 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Modals.FileBrowser
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.Settings.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property bool greeterFprintToggleAvailable: SettingsData.fprintdAvailable || SettingsData.greeterEnableFprint
|
||||||
|
readonly property bool greeterU2fToggleAvailable: SettingsData.u2fAvailable || SettingsData.greeterEnableU2f
|
||||||
|
|
||||||
|
function refreshAuthDetection() {
|
||||||
|
SettingsData.refreshAuthAvailability();
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible)
|
||||||
|
refreshAuthDetection();
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfirmModal {
|
||||||
|
id: greeterActionConfirm
|
||||||
|
}
|
||||||
|
|
||||||
|
FileBrowserModal {
|
||||||
|
id: greeterWallpaperBrowserModal
|
||||||
|
browserTitle: I18n.tr("Select greeter background image")
|
||||||
|
browserIcon: "wallpaper"
|
||||||
|
browserType: "wallpaper"
|
||||||
|
showHiddenFiles: true
|
||||||
|
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif"]
|
||||||
|
onFileSelected: path => {
|
||||||
|
SettingsData.set("greeterWallpaperPath", path);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property string greeterStatusText: ""
|
||||||
|
property bool greeterStatusRunning: false
|
||||||
|
property bool greeterSyncRunning: false
|
||||||
|
property bool greeterInstallActionRunning: false
|
||||||
|
property string greeterStatusStdout: ""
|
||||||
|
property string greeterStatusStderr: ""
|
||||||
|
property string greeterSyncStdout: ""
|
||||||
|
property string greeterSyncStderr: ""
|
||||||
|
property string greeterSudoProbeStderr: ""
|
||||||
|
property string greeterTerminalFallbackStderr: ""
|
||||||
|
property bool greeterTerminalFallbackFromPrecheck: false
|
||||||
|
property var cachedFontFamilies: []
|
||||||
|
property bool fontsEnumerated: false
|
||||||
|
property bool greeterBinaryExists: false
|
||||||
|
property bool greeterEnabled: false
|
||||||
|
readonly property bool greeterInstalled: greeterBinaryExists || greeterEnabled
|
||||||
|
|
||||||
|
readonly property string greeterActionLabel: {
|
||||||
|
if (!root.greeterInstalled)
|
||||||
|
return I18n.tr("Install");
|
||||||
|
if (!root.greeterEnabled)
|
||||||
|
return I18n.tr("Activate");
|
||||||
|
return I18n.tr("Uninstall");
|
||||||
|
}
|
||||||
|
readonly property string greeterActionIcon: {
|
||||||
|
if (!root.greeterInstalled)
|
||||||
|
return "download";
|
||||||
|
if (!root.greeterEnabled)
|
||||||
|
return "login";
|
||||||
|
return "delete";
|
||||||
|
}
|
||||||
|
readonly property var greeterActionCommand: {
|
||||||
|
if (!root.greeterInstalled)
|
||||||
|
return ["dms", "greeter", "install", "--terminal"];
|
||||||
|
if (!root.greeterEnabled)
|
||||||
|
return ["dms", "greeter", "enable", "--terminal"];
|
||||||
|
return ["dms", "greeter", "uninstall", "--terminal", "--yes"];
|
||||||
|
}
|
||||||
|
property string greeterPendingAction: ""
|
||||||
|
|
||||||
|
function checkGreeterInstallState() {
|
||||||
|
greetdEnabledCheckProcess.running = true;
|
||||||
|
greeterBinaryCheckProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runGreeterStatus() {
|
||||||
|
greeterStatusText = "";
|
||||||
|
greeterStatusStdout = "";
|
||||||
|
greeterStatusStderr = "";
|
||||||
|
greeterStatusRunning = true;
|
||||||
|
greeterStatusProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runGreeterInstallAction() {
|
||||||
|
root.greeterPendingAction = !root.greeterInstalled ? "install" : !root.greeterEnabled ? "activate" : "uninstall";
|
||||||
|
greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "…";
|
||||||
|
greeterInstallActionRunning = true;
|
||||||
|
greeterInstallActionProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptGreeterActionConfirm() {
|
||||||
|
var title, message, confirmText;
|
||||||
|
if (!root.greeterInstalled) {
|
||||||
|
title = I18n.tr("Install Greeter", "greeter action confirmation");
|
||||||
|
message = I18n.tr("Install the DMS greeter? A terminal will open for sudo authentication.");
|
||||||
|
confirmText = I18n.tr("Install");
|
||||||
|
} else if (!root.greeterEnabled) {
|
||||||
|
title = I18n.tr("Activate Greeter", "greeter action confirmation");
|
||||||
|
message = I18n.tr("Activate the DMS greeter? A terminal will open for sudo authentication. Run Sync after activation to apply your settings.");
|
||||||
|
confirmText = I18n.tr("Activate");
|
||||||
|
} else {
|
||||||
|
title = I18n.tr("Uninstall Greeter", "greeter action confirmation");
|
||||||
|
message = I18n.tr("Uninstall the DMS greeter? This will remove configuration and restore your previous display manager. A terminal will open for sudo authentication.");
|
||||||
|
confirmText = I18n.tr("Uninstall");
|
||||||
|
}
|
||||||
|
greeterActionConfirm.showWithOptions({
|
||||||
|
"title": title,
|
||||||
|
"message": message,
|
||||||
|
"confirmText": confirmText,
|
||||||
|
"cancelText": I18n.tr("Cancel"),
|
||||||
|
"confirmColor": Theme.primary,
|
||||||
|
"onConfirm": () => root.runGreeterInstallAction(),
|
||||||
|
"onCancel": () => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runGreeterSync() {
|
||||||
|
greeterSyncStdout = "";
|
||||||
|
greeterSyncStderr = "";
|
||||||
|
greeterSudoProbeStderr = "";
|
||||||
|
greeterTerminalFallbackStderr = "";
|
||||||
|
greeterTerminalFallbackFromPrecheck = false;
|
||||||
|
greeterStatusText = I18n.tr("Checking whether sudo authentication is needed…");
|
||||||
|
greeterSyncRunning = true;
|
||||||
|
greeterSudoProbeProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchGreeterSyncTerminalFallback(fromPrecheck, statusText) {
|
||||||
|
greeterTerminalFallbackFromPrecheck = fromPrecheck;
|
||||||
|
if (statusText && statusText !== "")
|
||||||
|
greeterStatusText = statusText;
|
||||||
|
greeterTerminalFallbackStderr = "";
|
||||||
|
greeterTerminalFallbackProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enumerateFonts() {
|
||||||
|
if (fontsEnumerated)
|
||||||
|
return;
|
||||||
|
var fonts = [];
|
||||||
|
var availableFonts = Qt.fontFamilies();
|
||||||
|
for (var i = 0; i < availableFonts.length; i++) {
|
||||||
|
var fontName = availableFonts[i];
|
||||||
|
if (fontName.startsWith("."))
|
||||||
|
continue;
|
||||||
|
fonts.push(fontName);
|
||||||
|
}
|
||||||
|
fonts.sort();
|
||||||
|
fonts.unshift("Default");
|
||||||
|
cachedFontFamilies = fonts;
|
||||||
|
fontsEnumerated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
refreshAuthDetection();
|
||||||
|
Qt.callLater(enumerateFonts);
|
||||||
|
Qt.callLater(checkGreeterInstallState);
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greetdEnabledCheckProcess
|
||||||
|
command: ["systemctl", "is-enabled", "greetd"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterEnabled = text.trim() === "enabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterBinaryCheckProcess
|
||||||
|
command: ["sh", "-c", "test -f /usr/bin/dms-greeter || test -f /usr/local/bin/dms-greeter"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
root.greeterBinaryExists = (exitCode === 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterStatusProcess
|
||||||
|
command: ["dms", "greeter", "status"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.greeterStatusStdout = text || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterStatusStderr = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
root.greeterStatusRunning = false;
|
||||||
|
const out = (root.greeterStatusStdout || "").trim();
|
||||||
|
const err = (root.greeterStatusStderr || "").trim();
|
||||||
|
if (exitCode === 0) {
|
||||||
|
root.greeterStatusText = out !== "" ? out : I18n.tr("No status output.");
|
||||||
|
if (err !== "")
|
||||||
|
root.greeterStatusText = root.greeterStatusText + "\n\nstderr:\n" + err;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var failure = I18n.tr("Failed to run 'dms greeter status'. Ensure DMS is installed and dms is in PATH.", "greeter status error") + " (exit " + exitCode + ")";
|
||||||
|
if (out !== "")
|
||||||
|
failure = failure + "\n\n" + out;
|
||||||
|
if (err !== "")
|
||||||
|
failure = failure + "\n\nstderr:\n" + err;
|
||||||
|
root.greeterStatusText = failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterSyncProcess
|
||||||
|
command: ["dms", "greeter", "sync", "--yes"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterSyncStdout = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterSyncStderr = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
root.greeterSyncRunning = false;
|
||||||
|
const out = (root.greeterSyncStdout || "").trim();
|
||||||
|
const err = (root.greeterSyncStderr || "").trim();
|
||||||
|
if (exitCode === 0) {
|
||||||
|
var success = I18n.tr("Sync completed successfully.");
|
||||||
|
if (out !== "")
|
||||||
|
success = success + "\n\n" + out;
|
||||||
|
if (err !== "")
|
||||||
|
success = success + "\n\nstderr:\n" + err;
|
||||||
|
root.greeterStatusText = success;
|
||||||
|
} else {
|
||||||
|
var failure = I18n.tr("Sync failed in background mode. Trying terminal mode so you can authenticate interactively.") + " (exit " + exitCode + ")";
|
||||||
|
if (out !== "")
|
||||||
|
failure = failure + "\n\n" + out;
|
||||||
|
if (err !== "")
|
||||||
|
failure = failure + "\n\nstderr:\n" + err;
|
||||||
|
root.greeterStatusText = failure;
|
||||||
|
root.launchGreeterSyncTerminalFallback(false, "");
|
||||||
|
}
|
||||||
|
root.checkGreeterInstallState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterSudoProbeProcess
|
||||||
|
command: ["sudo", "-n", "true"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterSudoProbeStderr = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
const err = (root.greeterSudoProbeStderr || "").trim();
|
||||||
|
if (exitCode === 0) {
|
||||||
|
root.greeterStatusText = I18n.tr("Running greeter sync…");
|
||||||
|
greeterSyncProcess.running = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var authNeeded = I18n.tr("Sync needs sudo authentication. Opening terminal so you can use password or fingerprint.");
|
||||||
|
if (err !== "")
|
||||||
|
authNeeded = authNeeded + "\n\n" + err;
|
||||||
|
root.launchGreeterSyncTerminalFallback(true, authNeeded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterTerminalFallbackProcess
|
||||||
|
command: ["dms", "greeter", "sync", "--terminal", "--yes"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterTerminalFallbackStderr = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
root.greeterSyncRunning = false;
|
||||||
|
if (exitCode === 0) {
|
||||||
|
var launched = root.greeterTerminalFallbackFromPrecheck ? I18n.tr("Terminal opened. Complete sync authentication there; it will close automatically when done.") : I18n.tr("Terminal fallback opened. Complete sync there; it will close automatically when done.");
|
||||||
|
root.greeterStatusText = root.greeterStatusText ? root.greeterStatusText + "\n\n" + launched : launched;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var fallback = I18n.tr("Terminal fallback failed. Install one of the supported terminal emulators or run 'dms greeter sync' manually.") + " (exit " + exitCode + ")";
|
||||||
|
const err = (root.greeterTerminalFallbackStderr || "").trim();
|
||||||
|
if (err !== "")
|
||||||
|
fallback = fallback + "\n\nstderr:\n" + err;
|
||||||
|
root.greeterStatusText = root.greeterStatusText ? root.greeterStatusText + "\n\n" + fallback : fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterInstallActionProcess
|
||||||
|
command: root.greeterActionCommand
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
root.greeterInstallActionRunning = false;
|
||||||
|
const pending = root.greeterPendingAction;
|
||||||
|
root.greeterPendingAction = "";
|
||||||
|
if (exitCode === 0) {
|
||||||
|
if (pending === "install")
|
||||||
|
root.greeterStatusText = I18n.tr("Install complete. Greeter has been installed.");
|
||||||
|
else if (pending === "activate")
|
||||||
|
root.greeterStatusText = I18n.tr("Greeter activated. greetd is now enabled.");
|
||||||
|
else
|
||||||
|
root.greeterStatusText = I18n.tr("Uninstall complete. Greeter has been removed.");
|
||||||
|
} else {
|
||||||
|
root.greeterStatusText = I18n.tr("Action failed or terminal was closed.") + " (exit " + exitCode + ")";
|
||||||
|
}
|
||||||
|
root.checkGreeterInstallState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var _lockDateFormatPresets: [
|
||||||
|
{
|
||||||
|
format: "",
|
||||||
|
label: I18n.tr("System Default", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "ddd d",
|
||||||
|
label: I18n.tr("Day Date", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "ddd MMM d",
|
||||||
|
label: I18n.tr("Day Month Date", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "MMM d",
|
||||||
|
label: I18n.tr("Month Date", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "M/d",
|
||||||
|
label: I18n.tr("Numeric (M/D)", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "d/M",
|
||||||
|
label: I18n.tr("Numeric (D/M)", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "ddd d MMM yyyy",
|
||||||
|
label: I18n.tr("Full with Year", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "yyyy-MM-dd",
|
||||||
|
label: I18n.tr("ISO Date", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "dddd, MMMM d",
|
||||||
|
label: I18n.tr("Full Day & Month", "date format option")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
readonly property var _wallpaperFillModes: ["Stretch", "Fit", "Fill", "Tile", "TileVertically", "TileHorizontally", "Pad"]
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
contentHeight: mainColumn.height + Theme.spacingXL
|
||||||
|
contentWidth: width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
topPadding: 4
|
||||||
|
width: Math.min(550, parent.width - Theme.spacingL * 2)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "info"
|
||||||
|
title: I18n.tr("Greeter Status")
|
||||||
|
settingKey: "greeterStatus"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Check sync status on demand. Sync copies your theme, settings, PAM config, and wallpaper to the login screen in one step. Must run Sync to apply changes.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: Theme.spacingS
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: Math.min(180, statusTextArea.implicitHeight + Theme.spacingM * 2)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHighest
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: statusTextArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
text: root.greeterStatusRunning ? I18n.tr("Checking…", "greeter status loading") : (root.greeterStatusText || I18n.tr("Click Refresh to check status.", "greeter status placeholder"))
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.family: "monospace"
|
||||||
|
color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
verticalAlignment: Text.AlignTop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: root.greeterActionLabel
|
||||||
|
iconName: root.greeterActionIcon
|
||||||
|
horizontalPadding: Theme.spacingL
|
||||||
|
onClicked: root.promptGreeterActionConfirm()
|
||||||
|
enabled: !root.greeterInstallActionRunning && !root.greeterSyncRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: I18n.tr("Refresh")
|
||||||
|
iconName: "refresh"
|
||||||
|
horizontalPadding: Theme.spacingL
|
||||||
|
onClicked: root.runGreeterStatus()
|
||||||
|
enabled: !root.greeterStatusRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: I18n.tr("Sync")
|
||||||
|
iconName: "sync"
|
||||||
|
horizontalPadding: Theme.spacingL
|
||||||
|
onClicked: root.runGreeterSync()
|
||||||
|
enabled: root.greeterInstalled && !root.greeterSyncRunning && !root.greeterInstallActionRunning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "fingerprint"
|
||||||
|
title: I18n.tr("Login Authentication")
|
||||||
|
settingKey: "greeterAuth"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Enable fingerprint or security key for DMS Greeter. Run Sync to apply and configure PAM.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterEnableFprint"
|
||||||
|
tags: ["greeter", "fingerprint", "fprintd", "login", "auth"]
|
||||||
|
text: I18n.tr("Enable fingerprint at login")
|
||||||
|
description: {
|
||||||
|
if (!SettingsData.fprintdAvailable) {
|
||||||
|
if (SettingsData.greeterEnableFprint)
|
||||||
|
return I18n.tr("Enabled in settings, but fingerprint availability could not yet be confirmed. Re-open after enrolling fingerprints or reconnecting the reader.");
|
||||||
|
return I18n.tr("Not available — install fprintd and enroll fingerprints.");
|
||||||
|
}
|
||||||
|
return SettingsData.greeterEnableFprint ? I18n.tr("Run Sync to apply. Fingerprint-only login may not unlock GNOME Keyring.") : I18n.tr("Only off for DMS-managed PAM lines. If greetd includes system-auth/common-auth/password-auth with pam_fprintd, fingerprint still stays enabled.");
|
||||||
|
}
|
||||||
|
descriptionColor: SettingsData.fprintdAvailable ? Theme.surfaceVariantText : Theme.warning
|
||||||
|
checked: SettingsData.greeterEnableFprint
|
||||||
|
enabled: root.greeterFprintToggleAvailable
|
||||||
|
onToggled: checked => SettingsData.set("greeterEnableFprint", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterEnableU2f"
|
||||||
|
tags: ["greeter", "u2f", "security", "key", "login", "auth"]
|
||||||
|
text: I18n.tr("Enable security key at login")
|
||||||
|
description: {
|
||||||
|
if (!SettingsData.u2fAvailable) {
|
||||||
|
if (SettingsData.greeterEnableU2f)
|
||||||
|
return I18n.tr("Enabled in settings, but security key availability could not yet be confirmed. Re-open after enrolling keys or updating pam_u2f.");
|
||||||
|
return I18n.tr("Not available — install pam_u2f and enroll keys.");
|
||||||
|
}
|
||||||
|
return SettingsData.greeterEnableU2f ? I18n.tr("Run Sync to apply.") : I18n.tr("Disabled.");
|
||||||
|
}
|
||||||
|
descriptionColor: SettingsData.u2fAvailable ? Theme.surfaceVariantText : Theme.warning
|
||||||
|
checked: SettingsData.greeterEnableU2f
|
||||||
|
enabled: root.greeterU2fToggleAvailable
|
||||||
|
onToggled: checked => SettingsData.set("greeterEnableU2f", checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "palette"
|
||||||
|
title: I18n.tr("Greeter Appearance")
|
||||||
|
settingKey: "greeterAppearance"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Font")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
topPadding: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsDropdownRow {
|
||||||
|
settingKey: "greeterFontFamily"
|
||||||
|
tags: ["greeter", "font", "typography"]
|
||||||
|
text: I18n.tr("Greeter font")
|
||||||
|
description: I18n.tr("Font used on the login screen")
|
||||||
|
options: root.fontsEnumerated ? root.cachedFontFamilies : ["Default"]
|
||||||
|
currentValue: (!SettingsData.greeterFontFamily || SettingsData.greeterFontFamily === "" || SettingsData.greeterFontFamily === Theme.defaultFontFamily) ? "Default" : (SettingsData.greeterFontFamily || "Default")
|
||||||
|
enableFuzzySearch: true
|
||||||
|
popupWidthOffset: 100
|
||||||
|
maxPopupHeight: 400
|
||||||
|
onValueChanged: value => {
|
||||||
|
if (value === "Default")
|
||||||
|
SettingsData.set("greeterFontFamily", "");
|
||||||
|
else
|
||||||
|
SettingsData.set("greeterFontFamily", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Time format")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
topPadding: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterUse24Hour"
|
||||||
|
tags: ["greeter", "time", "24hour"]
|
||||||
|
text: I18n.tr("24-hour clock")
|
||||||
|
description: I18n.tr("Greeter only — does not affect main clock")
|
||||||
|
checked: SettingsData.greeterUse24HourClock
|
||||||
|
onToggled: checked => SettingsData.set("greeterUse24HourClock", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterShowSeconds"
|
||||||
|
tags: ["greeter", "time", "seconds"]
|
||||||
|
text: I18n.tr("Show seconds")
|
||||||
|
checked: SettingsData.greeterShowSeconds
|
||||||
|
onToggled: checked => SettingsData.set("greeterShowSeconds", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterPadHours"
|
||||||
|
tags: ["greeter", "time", "12hour"]
|
||||||
|
text: I18n.tr("Pad hours (02:00 vs 2:00)")
|
||||||
|
visible: !SettingsData.greeterUse24HourClock
|
||||||
|
checked: SettingsData.greeterPadHours12Hour
|
||||||
|
onToggled: checked => SettingsData.set("greeterPadHours12Hour", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Date format on greeter")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
topPadding: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsDropdownRow {
|
||||||
|
settingKey: "greeterLockDateFormat"
|
||||||
|
tags: ["greeter", "date", "format"]
|
||||||
|
text: I18n.tr("Date format")
|
||||||
|
description: I18n.tr("Greeter only — format for the date on the login screen")
|
||||||
|
options: root._lockDateFormatPresets.map(p => p.label)
|
||||||
|
currentValue: {
|
||||||
|
var current = (SettingsData.greeterLockDateFormat !== undefined && SettingsData.greeterLockDateFormat !== "") ? SettingsData.greeterLockDateFormat : SettingsData.lockDateFormat || "";
|
||||||
|
var match = root._lockDateFormatPresets.find(p => p.format === current);
|
||||||
|
return match ? match.label : (current ? I18n.tr("Custom: ") + current : root._lockDateFormatPresets[0].label);
|
||||||
|
}
|
||||||
|
onValueChanged: value => {
|
||||||
|
var preset = root._lockDateFormatPresets.find(p => p.label === value);
|
||||||
|
SettingsData.set("greeterLockDateFormat", preset ? preset.format : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Background")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
topPadding: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Use a custom image for the login screen, or leave empty to use your desktop wallpaper.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: greeterWallpaperPathField
|
||||||
|
width: parent.width - browseGreeterWallpaperButton.width - Theme.spacingS
|
||||||
|
placeholderText: I18n.tr("Use desktop wallpaper")
|
||||||
|
text: SettingsData.greeterWallpaperPath
|
||||||
|
backgroundColor: Theme.surfaceContainerHighest
|
||||||
|
onTextChanged: {
|
||||||
|
if (text !== SettingsData.greeterWallpaperPath)
|
||||||
|
SettingsData.set("greeterWallpaperPath", text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
id: browseGreeterWallpaperButton
|
||||||
|
text: I18n.tr("Browse")
|
||||||
|
horizontalPadding: Theme.spacingL
|
||||||
|
onClicked: greeterWallpaperBrowserModal.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsDropdownRow {
|
||||||
|
settingKey: "greeterWallpaperFillMode"
|
||||||
|
tags: ["greeter", "wallpaper", "background", "fill"]
|
||||||
|
text: I18n.tr("Wallpaper fill mode")
|
||||||
|
description: I18n.tr("How the background image is scaled")
|
||||||
|
options: root._wallpaperFillModes.map(m => I18n.tr(m, "wallpaper fill mode"))
|
||||||
|
currentValue: {
|
||||||
|
var mode = (SettingsData.greeterWallpaperFillMode && SettingsData.greeterWallpaperFillMode !== "") ? SettingsData.greeterWallpaperFillMode : (SettingsData.wallpaperFillMode || "Fill");
|
||||||
|
var idx = root._wallpaperFillModes.indexOf(mode);
|
||||||
|
return idx >= 0 ? I18n.tr(root._wallpaperFillModes[idx], "wallpaper fill mode") : I18n.tr("Fill", "wallpaper fill mode");
|
||||||
|
}
|
||||||
|
onValueChanged: value => {
|
||||||
|
var idx = root._wallpaperFillModes.map(m => I18n.tr(m, "wallpaper fill mode")).indexOf(value);
|
||||||
|
if (idx >= 0)
|
||||||
|
SettingsData.set("greeterWallpaperFillMode", root._wallpaperFillModes[idx]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Layout and module positions on the greeter are synced from your shell (e.g. bar config). Run Sync to apply.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
topPadding: Theme.spacingS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "history"
|
||||||
|
title: I18n.tr("Greeter Behavior")
|
||||||
|
settingKey: "greeterBehavior"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Convenience options for the login screen. Sync to apply.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterRememberLastSession"
|
||||||
|
tags: ["greeter", "session", "remember", "login"]
|
||||||
|
text: I18n.tr("Remember last session")
|
||||||
|
description: I18n.tr("Pre-select the last used session on the greeter")
|
||||||
|
checked: SettingsData.greeterRememberLastSession
|
||||||
|
onToggled: checked => SettingsData.set("greeterRememberLastSession", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterRememberLastUser"
|
||||||
|
tags: ["greeter", "user", "remember", "login", "username"]
|
||||||
|
text: I18n.tr("Remember last user")
|
||||||
|
description: I18n.tr("Pre-fill the last successful username on the greeter")
|
||||||
|
checked: SettingsData.greeterRememberLastUser
|
||||||
|
onToggled: checked => SettingsData.set("greeterRememberLastUser", checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "extension"
|
||||||
|
title: I18n.tr("Dependencies & documentation")
|
||||||
|
settingKey: "greeterDeps"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("DMS greeter needs: greetd, dms-greeter. Fingerprint: fprintd, pam_fprintd. Security keys: pam_u2f. Add your user to the greeter group. Sync checks sudo first and opens a terminal when interactive authentication is required.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Installation and PAM setup: see the ") + "<a href=\"https://danklinux.com/docs/dankgreeter/installation\" style=\"text-decoration:none; color:" + Theme.primary + ";\">DankGreeter docs</a> " + I18n.tr("or run ") + "'dms greeter install'."
|
||||||
|
textFormat: Text.RichText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
linkColor: Theme.primary
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
onLinkActivated: url => Qt.openUrlExternally(url)
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
propagateComposedEvents: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,19 @@ import qs.Modules.Settings.Widgets
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
readonly property bool lockFprintToggleAvailable: SettingsData.fprintdAvailable || SettingsData.enableFprint
|
||||||
|
readonly property bool lockU2fToggleAvailable: SettingsData.u2fAvailable || SettingsData.enableU2f
|
||||||
|
|
||||||
|
function refreshAuthDetection() {
|
||||||
|
SettingsData.refreshAuthAvailability();
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: refreshAuthDetection()
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible)
|
||||||
|
refreshAuthDetection();
|
||||||
|
}
|
||||||
|
|
||||||
FileBrowserModal {
|
FileBrowserModal {
|
||||||
id: videoBrowserModal
|
id: videoBrowserModal
|
||||||
browserTitle: I18n.tr("Select Video or Folder")
|
browserTitle: I18n.tr("Select Video or Folder")
|
||||||
@@ -172,10 +185,16 @@ Item {
|
|||||||
settingKey: "enableFprint"
|
settingKey: "enableFprint"
|
||||||
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
|
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
|
||||||
text: I18n.tr("Enable fingerprint authentication")
|
text: I18n.tr("Enable fingerprint authentication")
|
||||||
description: SettingsData.fprintdAvailable ? I18n.tr("Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)") : I18n.tr("Not enrolled", "fingerprint not detected status")
|
description: {
|
||||||
|
if (SettingsData.fprintdAvailable)
|
||||||
|
return I18n.tr("Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)");
|
||||||
|
if (SettingsData.enableFprint)
|
||||||
|
return I18n.tr("Enabled in settings, but fingerprint availability could not yet be confirmed. Re-open after enrolling fingerprints or reconnecting the reader.");
|
||||||
|
return I18n.tr("Not available — install fprintd and enroll fingerprints.");
|
||||||
|
}
|
||||||
descriptionColor: SettingsData.fprintdAvailable ? Theme.surfaceVariantText : Theme.warning
|
descriptionColor: SettingsData.fprintdAvailable ? Theme.surfaceVariantText : Theme.warning
|
||||||
checked: SettingsData.enableFprint
|
checked: SettingsData.enableFprint
|
||||||
enabled: SettingsData.fprintdAvailable
|
enabled: root.lockFprintToggleAvailable
|
||||||
onToggled: checked => SettingsData.set("enableFprint", checked)
|
onToggled: checked => SettingsData.set("enableFprint", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,10 +202,16 @@ Item {
|
|||||||
settingKey: "enableU2f"
|
settingKey: "enableU2f"
|
||||||
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "fido", "authentication", "hardware"]
|
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "fido", "authentication", "hardware"]
|
||||||
text: I18n.tr("Enable security key authentication", "Enable FIDO2/U2F hardware security key for lock screen")
|
text: I18n.tr("Enable security key authentication", "Enable FIDO2/U2F hardware security key for lock screen")
|
||||||
description: SettingsData.u2fAvailable ? I18n.tr("Use a FIDO2/U2F security key (e.g. YubiKey) for lock screen authentication (requires enrolled keys)", "lock screen U2F security key setting") : I18n.tr("Not enrolled", "security key not detected status")
|
description: {
|
||||||
|
if (SettingsData.u2fAvailable)
|
||||||
|
return I18n.tr("Use a FIDO2/U2F security key (e.g. YubiKey) for lock screen authentication (requires enrolled keys)", "lock screen U2F security key setting");
|
||||||
|
if (SettingsData.enableU2f)
|
||||||
|
return I18n.tr("Enabled in settings, but security key availability could not yet be confirmed. Re-open after enrolling keys or updating pam_u2f.");
|
||||||
|
return I18n.tr("Not available — install pam_u2f and enroll keys.");
|
||||||
|
}
|
||||||
descriptionColor: SettingsData.u2fAvailable ? Theme.surfaceVariantText : Theme.warning
|
descriptionColor: SettingsData.u2fAvailable ? Theme.surfaceVariantText : Theme.warning
|
||||||
checked: SettingsData.enableU2f
|
checked: SettingsData.enableU2f
|
||||||
enabled: SettingsData.u2fAvailable
|
enabled: root.lockU2fToggleAvailable
|
||||||
onToggled: checked => SettingsData.set("enableU2f", checked)
|
onToggled: checked => SettingsData.set("enableU2f", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +220,7 @@ Item {
|
|||||||
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
|
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
|
||||||
text: I18n.tr("Security key mode", "lock screen U2F security key mode setting")
|
text: I18n.tr("Security key mode", "lock screen U2F security key mode setting")
|
||||||
description: I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting")
|
description: I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting")
|
||||||
visible: SettingsData.u2fAvailable && SettingsData.enableU2f
|
visible: SettingsData.enableU2f
|
||||||
options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint")]
|
options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint")]
|
||||||
currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method")
|
currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method")
|
||||||
onValueChanged: value => {
|
onValueChanged: value => {
|
||||||
@@ -245,7 +270,7 @@ Item {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Path to a video file or folder containing videos")
|
text: I18n.tr("Path to a video file or folder containing videos")
|
||||||
font.pixelSize: Theme.fontSizeXSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.outlineVariant
|
color: Theme.outlineVariant
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|||||||
@@ -1935,11 +1935,6 @@ Item {
|
|||||||
label: I18n.tr("Auth Type"),
|
label: I18n.tr("Auth Type"),
|
||||||
value: data["connection-type"]
|
value: data["connection-type"]
|
||||||
});
|
});
|
||||||
fields.push({
|
|
||||||
label: I18n.tr("Autoconnect"),
|
|
||||||
value: configData.autoconnect ? I18n.tr("Yes") : I18n.tr("No")
|
|
||||||
});
|
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1978,6 +1973,16 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("Autoconnect")
|
||||||
|
checked: configData ? (configData.autoconnect || false) : false
|
||||||
|
visible: !VPNService.configLoading && configData !== null
|
||||||
|
onToggled: checked => {
|
||||||
|
VPNService.updateConfig(modelData.uuid, {autoconnect: checked});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: 1
|
width: 1
|
||||||
height: Theme.spacingXS
|
height: Theme.spacingXS
|
||||||
|
|||||||
@@ -288,6 +288,15 @@ Item {
|
|||||||
onToggled: checked => SettingsData.set("notificationPopupPrivacyMode", checked)
|
onToggled: checked => SettingsData.set("notificationPopupPrivacyMode", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "notificationFocusedMonitor"
|
||||||
|
tags: ["notification", "popup", "focused", "monitor", "display", "screen", "active"]
|
||||||
|
text: I18n.tr("Focused Monitor Only")
|
||||||
|
description: I18n.tr("Show notification popups only on the currently focused monitor")
|
||||||
|
checked: SettingsData.notificationFocusedMonitor
|
||||||
|
onToggled: checked => SettingsData.set("notificationFocusedMonitor", checked)
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: notificationAnimationColumn.implicitHeight + Theme.spacingM * 2
|
height: notificationAnimationColumn.implicitHeight + Theme.spacingM * 2
|
||||||
|
|||||||
@@ -408,6 +408,8 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
clip: true
|
clip: true
|
||||||
visible: !root.isLoading
|
visible: !root.isLoading
|
||||||
|
add: null
|
||||||
|
displaced: null
|
||||||
|
|
||||||
ScrollBar.vertical: DankScrollbar {
|
ScrollBar.vertical: DankScrollbar {
|
||||||
id: browserScrollbar
|
id: browserScrollbar
|
||||||
|
|||||||
@@ -2638,6 +2638,18 @@ Item {
|
|||||||
checked: SettingsData.matugenTemplateEmacs
|
checked: SettingsData.matugenTemplateEmacs
|
||||||
onToggled: checked => SettingsData.set("matugenTemplateEmacs", checked)
|
onToggled: checked => SettingsData.set("matugenTemplateEmacs", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tab: "theme"
|
||||||
|
tags: ["matugen", "zed", "template"]
|
||||||
|
settingKey: "matugenTemplateZed"
|
||||||
|
text: "Zed"
|
||||||
|
description: getTemplateDescription("zed", "")
|
||||||
|
descriptionColor: getTemplateDescriptionColor("zed")
|
||||||
|
visible: SettingsData.runDmsMatugenTemplates
|
||||||
|
checked: SettingsData.matugenTemplateZed
|
||||||
|
onToggled: checked => SettingsData.set("matugenTemplateZed", checked)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|||||||
@@ -125,6 +125,16 @@ Item {
|
|||||||
onToggled: checked => SettingsData.set("groupWorkspaceApps", checked)
|
onToggled: checked => SettingsData.set("groupWorkspaceApps", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "workspaceActiveAppHighlightEnabled"
|
||||||
|
tags: ["workspace", "apps", "icons", "highlight", "active", "focused"]
|
||||||
|
text: I18n.tr("Highlight Active Workspace App")
|
||||||
|
description: I18n.tr("Highlight the currently focused app inside workspace indicators")
|
||||||
|
checked: SettingsData.workspaceActiveAppHighlightEnabled
|
||||||
|
visible: SettingsData.showWorkspaceApps
|
||||||
|
onToggled: checked => SettingsData.set("workspaceActiveAppHighlightEnabled", checked)
|
||||||
|
}
|
||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
settingKey: "workspaceFollowFocus"
|
settingKey: "workspaceFollowFocus"
|
||||||
tags: ["workspace", "focus", "follow", "monitor"]
|
tags: ["workspace", "focus", "follow", "monitor"]
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ quickshell -p quickshell/
|
|||||||
**Code formatting:**
|
**Code formatting:**
|
||||||
```bash
|
```bash
|
||||||
qmlfmt -t 4 -i 4 -b 250 -w path/to/file.qml
|
qmlfmt -t 4 -i 4 -b 250 -w path/to/file.qml
|
||||||
qmllint **/*.qml
|
make lint-qml # Run from repo root; requires quickshell/.qmlls.ini (generated by `qs -p quickshell/`)
|
||||||
|
# Uses Qt 6 qmllint. Override path with QMLLINT=/path/to/qmllint if needed.
|
||||||
|
# Auto-detects `qmllint6`, Fedora's `qmllint-qt6`, `/usr/lib/qt6/bin/qmllint`, then `qmllint`.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Components
|
## Components
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|||||||
@@ -93,9 +93,9 @@ Singleton {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
monitorOffMonitor = Qt.createQmlObject(qmlString, root, "IdleService.MonitorOffMonitor");
|
monitorOffMonitor = Qt.createQmlObject(qmlString, root, "IdleService.MonitorOffMonitor");
|
||||||
monitorOffMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.monitorTimeout > 0);
|
monitorOffMonitor.timeout = Qt.binding(() => root.monitorTimeout > 0 ? root.monitorTimeout : 86400);
|
||||||
monitorOffMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
|
monitorOffMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
|
||||||
monitorOffMonitor.timeout = Qt.binding(() => root.monitorTimeout);
|
monitorOffMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.monitorTimeout > 0);
|
||||||
monitorOffMonitor.isIdleChanged.connect(function () {
|
monitorOffMonitor.isIdleChanged.connect(function () {
|
||||||
if (monitorOffMonitor.isIdle) {
|
if (monitorOffMonitor.isIdle) {
|
||||||
if (SettingsData.fadeToDpmsEnabled) {
|
if (SettingsData.fadeToDpmsEnabled) {
|
||||||
@@ -112,9 +112,9 @@ Singleton {
|
|||||||
});
|
});
|
||||||
|
|
||||||
lockMonitor = Qt.createQmlObject(qmlString, root, "IdleService.LockMonitor");
|
lockMonitor = Qt.createQmlObject(qmlString, root, "IdleService.LockMonitor");
|
||||||
lockMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.lockTimeout > 0);
|
lockMonitor.timeout = Qt.binding(() => root.lockTimeout > 0 ? root.lockTimeout : 86400);
|
||||||
lockMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
|
lockMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
|
||||||
lockMonitor.timeout = Qt.binding(() => root.lockTimeout);
|
lockMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.lockTimeout > 0);
|
||||||
lockMonitor.isIdleChanged.connect(function () {
|
lockMonitor.isIdleChanged.connect(function () {
|
||||||
if (lockMonitor.isIdle) {
|
if (lockMonitor.isIdle) {
|
||||||
if (SettingsData.fadeToLockEnabled) {
|
if (SettingsData.fadeToLockEnabled) {
|
||||||
@@ -130,9 +130,9 @@ Singleton {
|
|||||||
});
|
});
|
||||||
|
|
||||||
suspendMonitor = Qt.createQmlObject(qmlString, root, "IdleService.SuspendMonitor");
|
suspendMonitor = Qt.createQmlObject(qmlString, root, "IdleService.SuspendMonitor");
|
||||||
suspendMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.suspendTimeout > 0);
|
suspendMonitor.timeout = Qt.binding(() => root.suspendTimeout > 0 ? root.suspendTimeout : 86400);
|
||||||
suspendMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
|
suspendMonitor.respectInhibitors = Qt.binding(() => root.respectInhibitors);
|
||||||
suspendMonitor.timeout = Qt.binding(() => root.suspendTimeout);
|
suspendMonitor.enabled = Qt.binding(() => root._enableGate && root.enabled && root.idleMonitorAvailable && root.suspendTimeout > 0);
|
||||||
suspendMonitor.isIdleChanged.connect(function () {
|
suspendMonitor.isIdleChanged.connect(function () {
|
||||||
if (suspendMonitor.isIdle) {
|
if (suspendMonitor.isIdle) {
|
||||||
root.requestSuspend();
|
root.requestSuspend();
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
ToastService.showInfo(I18n.tr("VPN configuration updated"));
|
ToastService.showInfo(I18n.tr("VPN configuration updated"));
|
||||||
DMSNetworkService.refreshVpnProfiles();
|
DMSNetworkService.refreshVpnProfiles();
|
||||||
|
getConfig(uuid);
|
||||||
configUpdated();
|
configUpdated();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process) {
|
if (process) {
|
||||||
process.command = ["sh", "-c", `find "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
|
process.command = ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
|
||||||
process.targetScreenName = screenName;
|
process.targetScreenName = screenName;
|
||||||
process.currentWallpaper = currentWallpaper;
|
process.currentWallpaper = currentWallpaper;
|
||||||
process.goToPrevious = false;
|
process.goToPrevious = false;
|
||||||
@@ -272,7 +272,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use global process for fallback
|
// Use global process for fallback
|
||||||
cyclingProcess.command = ["sh", "-c", `find "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
|
cyclingProcess.command = ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
|
||||||
cyclingProcess.targetScreenName = screenName || "";
|
cyclingProcess.targetScreenName = screenName || "";
|
||||||
cyclingProcess.currentWallpaper = currentWallpaper;
|
cyclingProcess.currentWallpaper = currentWallpaper;
|
||||||
cyclingProcess.running = true;
|
cyclingProcess.running = true;
|
||||||
@@ -296,7 +296,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process) {
|
if (process) {
|
||||||
process.command = ["sh", "-c", `find "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
|
process.command = ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
|
||||||
process.targetScreenName = screenName;
|
process.targetScreenName = screenName;
|
||||||
process.currentWallpaper = currentWallpaper;
|
process.currentWallpaper = currentWallpaper;
|
||||||
process.goToPrevious = true;
|
process.goToPrevious = true;
|
||||||
@@ -304,7 +304,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use global process for fallback
|
// Use global process for fallback
|
||||||
prevCyclingProcess.command = ["sh", "-c", `find "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
|
prevCyclingProcess.command = ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
|
||||||
prevCyclingProcess.targetScreenName = screenName || "";
|
prevCyclingProcess.targetScreenName = screenName || "";
|
||||||
prevCyclingProcess.currentWallpaper = currentWallpaper;
|
prevCyclingProcess.currentWallpaper = currentWallpaper;
|
||||||
prevCyclingProcess.running = true;
|
prevCyclingProcess.running = true;
|
||||||
|
|||||||
@@ -18,258 +18,329 @@ Singleton {
|
|||||||
target: DMSService
|
target: DMSService
|
||||||
|
|
||||||
function onCapabilitiesReceived() {
|
function onCapabilitiesReceived() {
|
||||||
checkCapabilities()
|
checkCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onConnectionStateChanged() {
|
function onConnectionStateChanged() {
|
||||||
if (DMSService.isConnected) {
|
if (DMSService.isConnected) {
|
||||||
checkCapabilities()
|
checkCapabilities();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
wlrOutputAvailable = false
|
wlrOutputAvailable = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWlrOutputStateUpdate(data) {
|
function onWlrOutputStateUpdate(data) {
|
||||||
if (!wlrOutputAvailable) {
|
if (!wlrOutputAvailable) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
handleStateUpdate(data)
|
handleStateUpdate(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!DMSService.dmsAvailable) {
|
if (!DMSService.dmsAvailable) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
checkCapabilities()
|
checkCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkCapabilities() {
|
function checkCapabilities() {
|
||||||
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
|
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
|
||||||
wlrOutputAvailable = false
|
wlrOutputAvailable = false;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasWlrOutput = DMSService.capabilities.includes("wlroutput")
|
const hasWlrOutput = DMSService.capabilities.includes("wlroutput");
|
||||||
if (hasWlrOutput && !wlrOutputAvailable) {
|
if (hasWlrOutput && !wlrOutputAvailable) {
|
||||||
wlrOutputAvailable = true
|
wlrOutputAvailable = true;
|
||||||
console.info("WlrOutputService: wlr-output-management capability detected")
|
console.info("WlrOutputService: wlr-output-management capability detected");
|
||||||
requestState()
|
requestState();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasWlrOutput) {
|
if (!hasWlrOutput) {
|
||||||
wlrOutputAvailable = false
|
wlrOutputAvailable = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestState() {
|
function requestState() {
|
||||||
if (!DMSService.isConnected || !wlrOutputAvailable) {
|
if (!DMSService.isConnected || !wlrOutputAvailable) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DMSService.sendRequest("wlroutput.getState", null, response => {
|
DMSService.sendRequest("wlroutput.getState", null, response => {
|
||||||
if (!response.result) {
|
if (!response.result) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
handleStateUpdate(response.result)
|
handleStateUpdate(response.result);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStateUpdate(state) {
|
function handleStateUpdate(state) {
|
||||||
outputs = state.outputs || []
|
outputs = state.outputs || [];
|
||||||
serial = state.serial || 0
|
serial = state.serial || 0;
|
||||||
|
|
||||||
if (outputs.length === 0) {
|
if (outputs.length === 0) {
|
||||||
console.warn("WlrOutputService: Received empty outputs list")
|
console.warn("WlrOutputService: Received empty outputs list");
|
||||||
} else {
|
} else {
|
||||||
console.log("WlrOutputService: Updated with", outputs.length, "outputs, serial:", serial)
|
console.log("WlrOutputService: Updated with", outputs.length, "outputs, serial:", serial);
|
||||||
outputs.forEach((output, index) => {
|
outputs.forEach((output, index) => {
|
||||||
console.log("WlrOutputService: Output", index, "-", output.name,
|
console.log("WlrOutputService: Output", index, "-", output.name, "enabled:", output.enabled, "mode:", output.currentMode ? output.currentMode.width + "x" + output.currentMode.height + "@" + (output.currentMode.refresh / 1000) + "Hz" : "none");
|
||||||
"enabled:", output.enabled,
|
});
|
||||||
"mode:", output.currentMode ?
|
|
||||||
output.currentMode.width + "x" + output.currentMode.height + "@" +
|
|
||||||
(output.currentMode.refresh / 1000) + "Hz" : "none")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
stateChanged()
|
stateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOutput(name) {
|
function getOutput(name) {
|
||||||
for (const output of outputs) {
|
for (const output of outputs) {
|
||||||
if (output.name === name) {
|
if (output.name === name) {
|
||||||
return output
|
return output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEnabledOutputs() {
|
function getEnabledOutputs() {
|
||||||
return outputs.filter(output => output.enabled)
|
return outputs.filter(output => output.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyConfiguration(heads, callback) {
|
function applyConfiguration(heads, callback) {
|
||||||
if (!DMSService.isConnected || !wlrOutputAvailable) {
|
if (!DMSService.isConnected || !wlrOutputAvailable) {
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(false, "Not connected")
|
callback(false, "Not connected");
|
||||||
}
|
}
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("WlrOutputService: Applying configuration for", heads.length, "outputs")
|
console.log("WlrOutputService: Applying configuration for", heads.length, "outputs");
|
||||||
heads.forEach((head, index) => {
|
heads.forEach((head, index) => {
|
||||||
console.log("WlrOutputService: Head", index, "- name:", head.name,
|
console.log("WlrOutputService: Head", index, "- name:", head.name, "enabled:", head.enabled, "modeId:", head.modeId, "customMode:", JSON.stringify(head.customMode), "position:", JSON.stringify(head.position), "scale:", head.scale, "transform:", head.transform, "adaptiveSync:", head.adaptiveSync);
|
||||||
"enabled:", head.enabled,
|
});
|
||||||
"modeId:", head.modeId,
|
|
||||||
"customMode:", JSON.stringify(head.customMode),
|
|
||||||
"position:", JSON.stringify(head.position),
|
|
||||||
"scale:", head.scale,
|
|
||||||
"transform:", head.transform,
|
|
||||||
"adaptiveSync:", head.adaptiveSync)
|
|
||||||
})
|
|
||||||
|
|
||||||
DMSService.sendRequest("wlroutput.applyConfiguration", {
|
DMSService.sendRequest("wlroutput.applyConfiguration", {
|
||||||
"heads": heads
|
"heads": heads
|
||||||
}, response => {
|
}, response => {
|
||||||
const success = !response.error
|
const success = !response.error;
|
||||||
const message = response.error || response.result?.message || ""
|
const message = response.error || response.result?.message || "";
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
console.warn("WlrOutputService: applyConfiguration error:", response.error)
|
console.warn("WlrOutputService: applyConfiguration error:", response.error);
|
||||||
} else {
|
} else {
|
||||||
console.log("WlrOutputService: Configuration applied successfully")
|
console.log("WlrOutputService: Configuration applied successfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
configurationApplied(success, message)
|
configurationApplied(success, message);
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(success, message)
|
callback(success, message);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function testConfiguration(heads, callback) {
|
function testConfiguration(heads, callback) {
|
||||||
if (!DMSService.isConnected || !wlrOutputAvailable) {
|
if (!DMSService.isConnected || !wlrOutputAvailable) {
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(false, "Not connected")
|
callback(false, "Not connected");
|
||||||
}
|
}
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("WlrOutputService: Testing configuration for", heads.length, "outputs")
|
console.log("WlrOutputService: Testing configuration for", heads.length, "outputs");
|
||||||
|
|
||||||
DMSService.sendRequest("wlroutput.testConfiguration", {
|
DMSService.sendRequest("wlroutput.testConfiguration", {
|
||||||
"heads": heads
|
"heads": heads
|
||||||
}, response => {
|
}, response => {
|
||||||
const success = !response.error
|
const success = !response.error;
|
||||||
const message = response.error || response.result?.message || ""
|
const message = response.error || response.result?.message || "";
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
console.warn("WlrOutputService: testConfiguration error:", response.error)
|
console.warn("WlrOutputService: testConfiguration error:", response.error);
|
||||||
} else {
|
} else {
|
||||||
console.log("WlrOutputService: Configuration test passed")
|
console.log("WlrOutputService: Configuration test passed");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(success, message)
|
callback(success, message);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOutputEnabled(outputName, enabled, callback) {
|
function setOutputEnabled(outputName, enabled, callback) {
|
||||||
const output = getOutput(outputName)
|
const output = getOutput(outputName);
|
||||||
if (!output) {
|
if (!output) {
|
||||||
console.warn("WlrOutputService: Output not found:", outputName)
|
console.warn("WlrOutputService: Output not found:", outputName);
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(false, "Output not found")
|
callback(false, "Output not found");
|
||||||
}
|
}
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const heads = [{
|
const heads = [
|
||||||
"name": outputName,
|
{
|
||||||
"enabled": enabled
|
"name": outputName,
|
||||||
}]
|
"enabled": enabled
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
if (enabled && output.currentMode) {
|
if (enabled && output.currentMode) {
|
||||||
heads[0].modeId = output.currentMode.id
|
heads[0].modeId = output.currentMode.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyConfiguration(heads, callback)
|
applyConfiguration(heads, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOutputMode(outputName, modeId, callback) {
|
function setOutputMode(outputName, modeId, callback) {
|
||||||
const heads = [{
|
const heads = [
|
||||||
"name": outputName,
|
{
|
||||||
"enabled": true,
|
"name": outputName,
|
||||||
"modeId": modeId
|
"enabled": true,
|
||||||
}]
|
"modeId": modeId
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
applyConfiguration(heads, callback)
|
applyConfiguration(heads, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOutputCustomMode(outputName, width, height, refresh, callback) {
|
function setOutputCustomMode(outputName, width, height, refresh, callback) {
|
||||||
const heads = [{
|
const heads = [
|
||||||
"name": outputName,
|
{
|
||||||
"enabled": true,
|
"name": outputName,
|
||||||
"customMode": {
|
"enabled": true,
|
||||||
"width": width,
|
"customMode": {
|
||||||
"height": height,
|
"width": width,
|
||||||
"refresh": refresh
|
"height": height,
|
||||||
|
"refresh": refresh
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}]
|
];
|
||||||
|
|
||||||
applyConfiguration(heads, callback)
|
applyConfiguration(heads, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOutputPosition(outputName, x, y, callback) {
|
function setOutputPosition(outputName, x, y, callback) {
|
||||||
const heads = [{
|
const heads = [
|
||||||
"name": outputName,
|
{
|
||||||
"enabled": true,
|
"name": outputName,
|
||||||
"position": {
|
"enabled": true,
|
||||||
"x": x,
|
"position": {
|
||||||
"y": y
|
"x": x,
|
||||||
|
"y": y
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}]
|
];
|
||||||
|
|
||||||
applyConfiguration(heads, callback)
|
applyConfiguration(heads, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOutputScale(outputName, scale, callback) {
|
function setOutputScale(outputName, scale, callback) {
|
||||||
const heads = [{
|
const heads = [
|
||||||
"name": outputName,
|
{
|
||||||
"enabled": true,
|
"name": outputName,
|
||||||
"scale": scale
|
"enabled": true,
|
||||||
}]
|
"scale": scale
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
applyConfiguration(heads, callback)
|
applyConfiguration(heads, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOutputTransform(outputName, transform, callback) {
|
function setOutputTransform(outputName, transform, callback) {
|
||||||
const heads = [{
|
const heads = [
|
||||||
"name": outputName,
|
{
|
||||||
"enabled": true,
|
"name": outputName,
|
||||||
"transform": transform
|
"enabled": true,
|
||||||
}]
|
"transform": transform
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
applyConfiguration(heads, callback)
|
applyConfiguration(heads, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOutputAdaptiveSync(outputName, state, callback) {
|
function setOutputAdaptiveSync(outputName, state, callback) {
|
||||||
const heads = [{
|
const heads = [
|
||||||
"name": outputName,
|
{
|
||||||
"enabled": true,
|
"name": outputName,
|
||||||
"adaptiveSync": state
|
"enabled": true,
|
||||||
}]
|
"adaptiveSync": state
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
applyConfiguration(heads, callback)
|
applyConfiguration(heads, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function configureOutput(config, callback) {
|
function configureOutput(config, callback) {
|
||||||
const heads = [config]
|
const heads = [config];
|
||||||
applyConfiguration(heads, callback)
|
applyConfiguration(heads, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function configureMultipleOutputs(configs, callback) {
|
function configureMultipleOutputs(configs, callback) {
|
||||||
applyConfiguration(configs, callback)
|
applyConfiguration(configs, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// High-level apply matching the generateOutputsConfig() pattern used by
|
||||||
|
// NiriService, HyprlandService and DwlService. Instead of writing a
|
||||||
|
// config file, the changes are applied directly via the
|
||||||
|
// wlr-output-management protocol.
|
||||||
|
function applyOutputsConfig(outputsData, connectedOutputs) {
|
||||||
|
if (!wlrOutputAvailable)
|
||||||
|
return;
|
||||||
|
const heads = [];
|
||||||
|
for (const name in outputsData) {
|
||||||
|
if (!connectedOutputs[name])
|
||||||
|
continue;
|
||||||
|
const output = outputsData[name];
|
||||||
|
const mode = (output.modes && output.current_mode >= 0) ? output.modes[output.current_mode] : null;
|
||||||
|
const enabled = !!mode;
|
||||||
|
const head = {
|
||||||
|
"name": name,
|
||||||
|
"enabled": enabled
|
||||||
|
};
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
if (mode.id !== undefined)
|
||||||
|
head.modeId = mode.id;
|
||||||
|
else
|
||||||
|
head.customMode = {
|
||||||
|
"width": mode.width,
|
||||||
|
"height": mode.height,
|
||||||
|
"refresh": mode.refresh_rate
|
||||||
|
};
|
||||||
|
|
||||||
|
if (output.logical) {
|
||||||
|
head.position = {
|
||||||
|
"x": output.logical.x ?? 0,
|
||||||
|
"y": output.logical.y ?? 0
|
||||||
|
};
|
||||||
|
head.scale = output.logical.scale ?? 1.0;
|
||||||
|
head.transform = transformFromName(output.logical.transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
heads.push(head);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heads.length > 0)
|
||||||
|
applyConfiguration(heads);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformFromName(name) {
|
||||||
|
switch (name) {
|
||||||
|
case "Normal":
|
||||||
|
return 0;
|
||||||
|
case "90":
|
||||||
|
return 1;
|
||||||
|
case "180":
|
||||||
|
return 2;
|
||||||
|
case "270":
|
||||||
|
return 3;
|
||||||
|
case "Flipped":
|
||||||
|
return 4;
|
||||||
|
case "Flipped90":
|
||||||
|
return 5;
|
||||||
|
case "Flipped180":
|
||||||
|
return 6;
|
||||||
|
case "Flipped270":
|
||||||
|
return 7;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,12 @@ Item {
|
|||||||
dropdownMenu.close();
|
dropdownMenu.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dropdownMenu.open();
|
dropdownMenu.open();
|
||||||
|
|
||||||
|
let currentIndex = root.options.indexOf(root.currentValue);
|
||||||
|
listView.positionViewAtIndex(currentIndex, ListView.Beginning);
|
||||||
|
|
||||||
const pos = dropdown.mapToItem(Overlay.overlay, 0, 0);
|
const pos = dropdown.mapToItem(Overlay.overlay, 0, 0);
|
||||||
const popupW = dropdownMenu.width;
|
const popupW = dropdownMenu.width;
|
||||||
const popupH = dropdownMenu.height;
|
const popupH = dropdownMenu.height;
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ Item {
|
|||||||
StyledRect {
|
StyledRect {
|
||||||
id: valueTooltip
|
id: valueTooltip
|
||||||
|
|
||||||
width: tooltipText.contentWidth + Theme.spacingS * 2
|
width: tooltipText.reservedWidth + Theme.spacingS * 2
|
||||||
height: tooltipText.contentHeight + Theme.spacingXS * 2
|
height: tooltipText.contentHeight + Theme.spacingXS * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceContainer
|
color: Theme.surfaceContainer
|
||||||
@@ -251,10 +251,22 @@ Item {
|
|||||||
visible: slider.alwaysShowValue ? slider.showValue : ((sliderMouseArea.containsMouse && slider.showValue) || (slider.isDragging && slider.showValue))
|
visible: slider.alwaysShowValue ? slider.showValue : ((sliderMouseArea.containsMouse && slider.showValue) || (slider.isDragging && slider.showValue))
|
||||||
opacity: visible ? 1 : 0
|
opacity: visible ? 1 : 0
|
||||||
|
|
||||||
StyledText {
|
NumericText {
|
||||||
id: tooltipText
|
id: tooltipText
|
||||||
|
|
||||||
text: (slider.valueOverride >= 0 ? Math.round(slider.valueOverride) : slider.value) + slider.unit
|
text: (slider.valueOverride >= 0 ? Math.round(slider.valueOverride) : slider.value) + slider.unit
|
||||||
|
reserveText: {
|
||||||
|
let widest = "";
|
||||||
|
const samples = [slider.minimum, slider.maximum];
|
||||||
|
if (slider.valueOverride >= 0)
|
||||||
|
samples.push(slider.valueOverride);
|
||||||
|
for (let i = 0; i < samples.length; i++) {
|
||||||
|
const candidate = Math.round(samples[i]) + slider.unit;
|
||||||
|
if (candidate.length > widest.length)
|
||||||
|
widest = candidate;
|
||||||
|
}
|
||||||
|
return widest;
|
||||||
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string reserveText: ""
|
||||||
|
readonly property real reservedWidth: reserveText !== "" ? Math.max(contentWidth, reserveMetrics.width) : contentWidth
|
||||||
|
|
||||||
|
isMonospace: true
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
|
||||||
|
StyledTextMetrics {
|
||||||
|
id: reserveMetrics
|
||||||
|
isMonospace: root.isMonospace
|
||||||
|
font.pixelSize: root.font.pixelSize
|
||||||
|
font.family: root.font.family
|
||||||
|
font.weight: root.font.weight
|
||||||
|
font.hintingPreference: root.font.hintingPreference
|
||||||
|
text: root.reserveText
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,11 +75,6 @@ Rectangle {
|
|||||||
"label": I18n.tr("Auth Type"),
|
"label": I18n.tr("Auth Type"),
|
||||||
"value": data["connection-type"]
|
"value": data["connection-type"]
|
||||||
});
|
});
|
||||||
fields.push({
|
|
||||||
"key": "auto",
|
|
||||||
"label": I18n.tr("Autoconnect"),
|
|
||||||
"value": configData.autoconnect ? I18n.tr("Yes") : I18n.tr("No")
|
|
||||||
});
|
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +266,16 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("Autoconnect")
|
||||||
|
checked: configData ? (configData.autoconnect || false) : false
|
||||||
|
visible: !VPNService.configLoading && configData !== null
|
||||||
|
onToggled: checked => {
|
||||||
|
VPNService.updateConfig(profile.uuid, {autoconnect: checked});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: 1
|
width: 1
|
||||||
height: Theme.spacingXS
|
height: Theme.spacingXS
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#%PAM-1.0
|
#%PAM-1.0
|
||||||
|
|
||||||
auth required pam_fprintd.so max-tries=1
|
auth required pam_fprintd.so max-tries=1 timeout=5
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[templates.dmszed]
|
||||||
|
input_path = "SHELL_DIR/matugen/templates/dank-zed.json"
|
||||||
|
output_path = "CONFIG_DIR/zed/themes/dank-zed-theme.json"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
[colors]
|
[colors-dark]
|
||||||
foreground={{colors.on_surface.default.hex_stripped}}
|
foreground={{colors.on_surface.default.hex_stripped}}
|
||||||
background={{colors.background.default.hex_stripped}}
|
background={{colors.background.default.hex_stripped}}
|
||||||
selection-foreground={{colors.on_surface.default.hex_stripped}}
|
selection-foreground={{colors.on_surface.default.hex_stripped}}
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ LANGUAGES = {
|
|||||||
"hu": "hu.json",
|
"hu": "hu.json",
|
||||||
"fa": "fa.json",
|
"fa": "fa.json",
|
||||||
"fr": "fr.json",
|
"fr": "fr.json",
|
||||||
"nl": "nl.json"
|
"nl": "nl.json",
|
||||||
|
"ru": "ru.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
def error(msg):
|
def error(msg):
|
||||||
|
|||||||
Executable
+129
@@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
script_dir="$(
|
||||||
|
CDPATH=''
|
||||||
|
cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd
|
||||||
|
)"
|
||||||
|
repo_root="$(
|
||||||
|
CDPATH=''
|
||||||
|
cd -- "${script_dir}/../.." && pwd
|
||||||
|
)"
|
||||||
|
quickshell_dir="${repo_root}/quickshell"
|
||||||
|
qmlls_config="${quickshell_dir}/.qmlls.ini"
|
||||||
|
|
||||||
|
# Resolve qmllint: honour QMLLINT, then try common Qt 6 binary names and
|
||||||
|
# install paths, and finally bare qmllint. We need the Qt 6 build (>= 6.x)
|
||||||
|
# because older Qt 5 qmllint doesn't understand --ignore-settings / -W.
|
||||||
|
resolve_qmllint() {
|
||||||
|
if [[ -n "${QMLLINT:-}" ]]; then
|
||||||
|
printf '%s\n' "${QMLLINT}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local candidate
|
||||||
|
for candidate in qmllint6 qmllint-qt6 /usr/lib/qt6/bin/qmllint qmllint; do
|
||||||
|
if command -v -- "${candidate}" >/dev/null 2>&1; then
|
||||||
|
printf '%s\n' "${candidate}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! qmllint_bin="$(resolve_qmllint)"; then
|
||||||
|
printf 'error: qmllint (Qt 6) not found in PATH (override with QMLLINT=/path/to/qmllint)\n' >&2
|
||||||
|
exit 127
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_broken_qmlls_link() {
|
||||||
|
local target=""
|
||||||
|
target="$(readlink -- "${qmlls_config}" 2>/dev/null || true)"
|
||||||
|
printf 'error: %s is a broken symlink. lint-qml requires a live Quickshell tooling VFS.\n' "${qmlls_config}" >&2
|
||||||
|
if [[ -n "${target}" ]]; then
|
||||||
|
printf 'Broken target: %s\n' "${target}" >&2
|
||||||
|
fi
|
||||||
|
print_vfs_recovery
|
||||||
|
}
|
||||||
|
|
||||||
|
trim_ini_value() {
|
||||||
|
local value="$1"
|
||||||
|
value="${value#\"}"
|
||||||
|
value="${value%\"}"
|
||||||
|
printf '%s\n' "${value}"
|
||||||
|
}
|
||||||
|
|
||||||
|
read_ini_value() {
|
||||||
|
local key="$1"
|
||||||
|
local file="$2"
|
||||||
|
local raw
|
||||||
|
|
||||||
|
raw="$(sed -n "s/^${key}=//p" "${file}" | head -n 1)"
|
||||||
|
if [[ -z "${raw}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
trim_ini_value "${raw}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_vfs_recovery() {
|
||||||
|
printf 'Generate it by starting the local shell config once, for example:\n' >&2
|
||||||
|
printf ' dms -c %q run\n' "${quickshell_dir}" >&2
|
||||||
|
printf ' qs -p %q\n' "${quickshell_dir}" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -L "${qmlls_config}" && ! -e "${qmlls_config}" ]]; then
|
||||||
|
print_broken_qmlls_link
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -e "${qmlls_config}" ]]; then
|
||||||
|
printf 'error: %s is missing. lint-qml requires the Quickshell tooling VFS.\n' "${qmlls_config}" >&2
|
||||||
|
print_vfs_recovery
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! build_dir="$(read_ini_value "buildDir" "${qmlls_config}")"; then
|
||||||
|
printf 'error: %s does not contain a buildDir entry.\n' "${qmlls_config}" >&2
|
||||||
|
print_vfs_recovery
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! import_paths_raw="$(read_ini_value "importPaths" "${qmlls_config}")"; then
|
||||||
|
printf 'error: %s does not contain an importPaths entry.\n' "${qmlls_config}" >&2
|
||||||
|
print_vfs_recovery
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "${build_dir}" || ! -f "${build_dir}/qs/qmldir" ]]; then
|
||||||
|
printf 'error: Quickshell tooling VFS is missing or stale: %s\n' "${build_dir}" >&2
|
||||||
|
print_vfs_recovery
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
targets=(
|
||||||
|
"${quickshell_dir}/shell.qml"
|
||||||
|
"${quickshell_dir}/DMSShell.qml"
|
||||||
|
"${quickshell_dir}/DMSGreeter.qml"
|
||||||
|
)
|
||||||
|
|
||||||
|
qmllint_args=(
|
||||||
|
--ignore-settings
|
||||||
|
-W 0
|
||||||
|
-I "${build_dir}"
|
||||||
|
)
|
||||||
|
|
||||||
|
IFS=':' read -r -a import_paths <<< "${import_paths_raw}"
|
||||||
|
for path in "${import_paths[@]}"; do
|
||||||
|
if [[ -n "${path}" ]]; then
|
||||||
|
qmllint_args+=(-I "${path}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! output="$("${qmllint_bin}" "${qmllint_args[@]}" "${targets[@]}" 2>&1)"; then
|
||||||
|
printf '%s\n' "${output}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${output}" ]]; then
|
||||||
|
printf '%s\n' "${output}"
|
||||||
|
fi
|
||||||
+1438
-706
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -978,6 +978,32 @@
|
|||||||
],
|
],
|
||||||
"description": "Group repeated application icons in unfocused workspaces"
|
"description": "Group repeated application icons in unfocused workspaces"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "workspaceActiveAppHighlightEnabled",
|
||||||
|
"label": "Highlight Active Workspace App",
|
||||||
|
"tabIndex": 4,
|
||||||
|
"category": "Workspaces",
|
||||||
|
"keywords": [
|
||||||
|
"active",
|
||||||
|
"app",
|
||||||
|
"apps",
|
||||||
|
"currently",
|
||||||
|
"day",
|
||||||
|
"desktop",
|
||||||
|
"focused",
|
||||||
|
"highlight",
|
||||||
|
"icons",
|
||||||
|
"indicators",
|
||||||
|
"inside",
|
||||||
|
"light mode",
|
||||||
|
"spaces",
|
||||||
|
"virtual",
|
||||||
|
"virtual desktops",
|
||||||
|
"workspace",
|
||||||
|
"workspaces"
|
||||||
|
],
|
||||||
|
"description": "Highlight the currently focused app inside workspace indicators"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "workspaceIcons",
|
"section": "workspaceIcons",
|
||||||
"label": "Named Workspace Icons",
|
"label": "Named Workspace Icons",
|
||||||
@@ -1281,7 +1307,8 @@
|
|||||||
"windows"
|
"windows"
|
||||||
],
|
],
|
||||||
"icon": "apps",
|
"icon": "apps",
|
||||||
"description": "Only show windows from the current monitor on each dock"
|
"description": "Only show windows from the current monitor on each dock",
|
||||||
|
"conditionKey": "isHyprland"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"section": "dockBorder",
|
"section": "dockBorder",
|
||||||
@@ -1550,6 +1577,33 @@
|
|||||||
],
|
],
|
||||||
"icon": "swap_vert"
|
"icon": "swap_vert"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "dockRestoreSpecialWorkspaceOnClick",
|
||||||
|
"label": "Restore Special Workspace Windows",
|
||||||
|
"tabIndex": 5,
|
||||||
|
"category": "Dock",
|
||||||
|
"keywords": [
|
||||||
|
"back",
|
||||||
|
"before",
|
||||||
|
"bring",
|
||||||
|
"clicking",
|
||||||
|
"desktop",
|
||||||
|
"dock",
|
||||||
|
"focusing",
|
||||||
|
"hyprland",
|
||||||
|
"launcher bar",
|
||||||
|
"panel",
|
||||||
|
"restore",
|
||||||
|
"special",
|
||||||
|
"taskbar",
|
||||||
|
"virtual",
|
||||||
|
"window",
|
||||||
|
"windows",
|
||||||
|
"workspace"
|
||||||
|
],
|
||||||
|
"description": "When clicking a dock window in a Hyprland special workspace, bring that special workspace back before focusing the window",
|
||||||
|
"conditionKey": "isHyprland"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "showDock",
|
"section": "showDock",
|
||||||
"label": "Show Dock",
|
"label": "Show Dock",
|
||||||
@@ -3781,6 +3835,23 @@
|
|||||||
],
|
],
|
||||||
"description": "Rounded corners for windows (decoration.rounding)"
|
"description": "Rounded corners for windows (decoration.rounding)"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "matugenTemplateZed",
|
||||||
|
"label": "Zed",
|
||||||
|
"tabIndex": 10,
|
||||||
|
"category": "Theme & Colors",
|
||||||
|
"keywords": [
|
||||||
|
"appearance",
|
||||||
|
"colors",
|
||||||
|
"look",
|
||||||
|
"matugen",
|
||||||
|
"scheme",
|
||||||
|
"style",
|
||||||
|
"template",
|
||||||
|
"theme",
|
||||||
|
"zed"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "matugenTemplateDgop",
|
"section": "matugenTemplateDgop",
|
||||||
"label": "dgop",
|
"label": "dgop",
|
||||||
@@ -5167,6 +5238,31 @@
|
|||||||
],
|
],
|
||||||
"description": "Save dismissed notifications to history"
|
"description": "Save dismissed notifications to history"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "notificationFocusedMonitor",
|
||||||
|
"label": "Focused Monitor Only",
|
||||||
|
"tabIndex": 17,
|
||||||
|
"category": "Notifications",
|
||||||
|
"keywords": [
|
||||||
|
"active",
|
||||||
|
"alert",
|
||||||
|
"alerts",
|
||||||
|
"currently",
|
||||||
|
"display",
|
||||||
|
"focused",
|
||||||
|
"messages",
|
||||||
|
"monitor",
|
||||||
|
"notif",
|
||||||
|
"notification",
|
||||||
|
"notifications",
|
||||||
|
"popup",
|
||||||
|
"popups",
|
||||||
|
"screen",
|
||||||
|
"show",
|
||||||
|
"toast"
|
||||||
|
],
|
||||||
|
"description": "Show notification popups only on the currently focused monitor"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "notificationHistoryMaxAgeDays",
|
"section": "notificationHistoryMaxAgeDays",
|
||||||
"label": "History Retention",
|
"label": "History Retention",
|
||||||
@@ -6828,5 +6924,16 @@
|
|||||||
"time"
|
"time"
|
||||||
],
|
],
|
||||||
"description": "Change the locale used for date and time formatting, independent of the interface language."
|
"description": "Change the locale used for date and time formatting, independent of the interface language."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "_tab_31",
|
||||||
|
"label": "Greeter",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Settings",
|
||||||
|
"keywords": [
|
||||||
|
"greeter",
|
||||||
|
"settings"
|
||||||
|
],
|
||||||
|
"icon": "login"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user